Skip to content

fix(lockfile): diagnose merge conflicts without automatic recovery - #3028

Open
Lachlan Heywood (lachieh) wants to merge 2 commits into
microsoft:mainfrom
lachieh:install-on-invalid-lockfile
Open

Lachlan Heywood (lachieh) wants to merge 2 commits into
microsoft:mainfrom
lachieh:install-on-invalid-lockfile

Conversation

@lachieh

@lachieh Lachlan Heywood (lachieh) commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

fix(lockfile): diagnose merge conflicts without automatic recovery

TL;DR

Report unresolved lockfile conflict markers through the canonical loader, with
the actual path, a redacted cause and manual resolve-or-restore guidance.
Retry the original command after repair; do not substitute an unqualified
install or a Git command that works only during a live merge.
The diagnostic leaves conflicted bytes untouched and preserves existing
frozen, preview, best-effort and unrelated-corruption behavior.

Important

This PR delivers only the approved diagnostic slice.
Refs #2979; it does not close that issue or implement automatic recovery.
Human agreement to the exact req-lk-023 text
is recorded separately from workflow execution and PR review.
Renewed human PR review remains required.

Problem (WHY)

  • Parser failures did not consistently identify unresolved conflict markers
    at the canonical load boundary; some callers offered commands that needed to
    read the same unreadable file.
  • The intermediate Git recipe depended on an active merge and replaced the
    caller's scope/options with plain apm install.
  • [!] Text-mode preservation assertions could miss CRLF-to-LF rewrites.

Contract-specific guidance follows
"Add what the agent lacks, omit what it knows".
The regression evidence follows the validation loop:
"do the work, run a validator (a script, a reference checklist, or a self-check), fix any issues, and repeat until validation passes.".

Approach (WHAT)

  • Recognize two-way and diff3 marker lines inside LockFile.read, before YAML
    parsing and within its existing error-normalization boundary.
  • Report the diagnosed path and manual repair, then retry the original command.
  • Route read-only export through the existing filename resolver and loader.
  • Keep the existing caller-specific exit and best-effort policies.
  • Prove exact-byte preservation, path selection and canonical routing with
    executed tests and deliberate failing mutations.

Implementation (HOW)

File Scoped change
src/apm_cli/deps/lockfile.py Add LockfileConflictError and marker recognition; remove the intermediate shell-path/recipe helper. Keep read/parse normalization together.
src/apm_cli/commands/lock.py Delegate export filename selection and loading; explain legacy read-in-place behavior in help.
src/apm_cli/install/service.py Compose the canonical conflict diagnostic into frozen failures; preserve the author's normalization fix.
src/apm_cli/install/errors.py Do not append drift advice to missing/unreadable-lockfile errors with no drift reasons.
src/apm_cli/commands/install.py Emit the frozen tip only when one exists.
src/apm_cli/install/mcp/command.py Surface the safe conflict diagnostic; keep other integration-error redaction behavior.
src/apm_cli/install/presentation/dry_run.py Warn on conflict while retaining preview's successful, non-mutating outcome.
scripts/architecture_linter/checks/contracts_test_taxonomy.py Extend the registered read-only lockfile rule to export and canonical loading.
tests/integration/test_architecture_pack_lockfile_read.py Reject export filename/load bypasses without creating a new owner.
tests/unit/deps/test_lockfile_conflict_markers.py Marker/negative cases, path/action/redaction and LF/CRLF bytes; focused Windows selection.
tests/unit/install/test_frozen.py Frozen manual guidance and omission of inappropriate drift tips.
tests/integration/test_install_conflicted_lockfile_e2e.py Command-boundary tests for install variants, readers, global/ancestor scope, legacy precedence, manual restoration and best-effort discovery. Classified component, not subprocess E2E.
tests/spec_conformance/test_lockfile_reqs.py Requirement-linked diagnostics, explicit exit outcomes and byte comparisons.
docs/src/content/docs/specs/openapm-v0.1.md Diagnostic-only req-lk-023; relocate its illustrative grammar note and add a section bridge.
docs/public/specs/manifests/openapm-v0.1.requirements.yml Register the diagnostic requirement.
CONFORMANCE.md, CONFORMANCE.json Contributor's generated conformance records for the new requirement.
docs/src/content/docs/reference/cli/install.md Frozen manual repair guidance.
docs/src/content/docs/reference/cli/lock.md Current-filename precedence and legacy read-only export.
docs/src/content/docs/reference/lockfile-spec.md Diagnosis, preservation and unchanged reader policies.
docs/src/content/docs/troubleshooting/install-failures.md Manual resolve/restore, original options and limits of choosing one side.
packages/apm-guide/.apm/skills/apm-usage/commands.md Mirror legacy export behavior.
packages/apm-guide/.apm/skills/apm-usage/troubleshooting.md Mirror scoped repair and preservation guidance.
CHANGELOG.md One credited diagnostic entry with the spec reference.

The intermediate tests/utils/diagnostic_recipe.py is removed; tests no longer
extract or execute shell advice.
Original contributor commits and attribution are retained.

Diagrams

Legend: the canonical loader owns detection; callers keep their existing policy,
and dashed boxes identify the diagnostic/routing additions.

flowchart LR
    subgraph Input["Lockfile consumers"]
        C["install / frozen / preview / update / outdated"]
        E["lock_export"]
        P["resolve_lockfile_path_for_read"]
        E --> P
    end
    subgraph Owner["deps/lockfile.py"]
        R["LockFile.read"]
        M{"has_conflict_markers"}
        Y["from_yaml"]
        X["LockfileConflictError: path, cause, manual repair"]
        R --> M
        M -->|"no"| Y
        M -->|"yes"| X
    end
    C --> R
    P --> R
    X --> H["Caller keeps existing exit or best-effort policy; no lockfile rewrite"]
    classDef new stroke-dasharray: 5 5;
    class M,X,P new;
Loading

Trade-offs

  • Manual repair, not regeneration. The operator chooses how to reconcile
    records; automatic discard/recovery remains out of scope on [Feature] Automatic recovery of merge-conflicted lockfiles #2979.
  • Illustrative grammar, not a new normative parser grammar. The implementation
    recognizes seven <, > or | characters at line start followed by space/end
    of line. Bare ======= and inline value text remain negative cases.
  • Preserve existing command semantics. Preview still exits 0 with a warning;
    best-effort inventory discovery still returns an empty result. This PR does
    not add transaction rollback for prior MCP manifest writes or redesign legacy
    migration in mutating commands.
  • No unrelated architecture or heading changes. Extend the existing owner
    guard; keep the Section 5.4 slug and all agreed normative obligations unchanged.

Benefits

  1. Conflicted reads identify the actual file and cause without parser excerpts.
  2. Manual repair guidance works outside an active Git merge and preserves the
    user's original global/frozen/preview invocation.
  3. LF and CRLF inputs are compared as bytes across command boundaries.
  4. Read-only export handles legacy files without migration, with current filename
    precedence and a static guard against bypassing the loader.

Validation

At 05dafde1c3799d34eeebe04b10109c50b220550b:

uv run --frozen --extra dev pytest -q tests/unit/deps/test_lockfile_conflict_markers.py tests/unit/install/test_frozen.py tests/integration/test_install_conflicted_lockfile_e2e.py tests/spec_conformance/test_lockfile_reqs.py tests/integration/test_architecture_pack_lockfile_read.py

114 passed, 1 skipped in 4.46s
Additional executed evidence

The same code before commit, with the owner-mutation matrix and tests/quality:

543 passed, 1 skipped in 242.38s (0:04:02)

Canonical Ruff check/format:

All checks passed!
1884 files already formatted

Pylint R0801, auth boundary, architecture boundary, YAML I/O, 2100-line and
portable-relative-path guards passed. The three grep/awk guards were also
checked using equivalent Python patterns on macOS.
The deterministic owner gate identified three touched owners and verified
executed functional evidence for each at the exact committed head.

Deliberate mutations failed as expected: loader bypass (5 failures, including
the architecture assertion), byte rewriting (19), reversed filename precedence
(2), and the old shell recipe (14). Every mutation was restored before the
passing runs. The Mermaid block was rendered successfully by local mmdc.

Mechanical spec checks passed for ASCII, forbidden tokens, five schemas,
17 fixtures, 124 unique requirement anchors, count sites, links, requirement
citations and the changelog reference. No Mermaid occurs in the spec itself.
The req-lk-023 clauses match the human-agreed revision byte-for-byte after
excluding the relocated non-normative note.

Prior-head GitHub checks at a9c6e13 succeeded after separate human workflow
authorization. Those are not CI evidence for 05dafde1c3; current-head checks
and renewed human review must be observed separately.

Scenario Evidence

# Scenario (user promise) Principle(s) Test(s) proving it Type
1 Get a named cause and manual repair, with unchanged LF/CRLF bytes DevX, Governed by policy tests/unit/deps/test_lockfile_conflict_markers.py::test_read_names_the_file_and_a_manual_next_step (regression-trap for #2979) component
2 Frozen and partial installs fail without changing conflicted bytes Governed by policy tests/integration/test_install_conflicted_lockfile_e2e.py::test_commands_fail_closed_and_preserve_the_lockfile component
3 Preview warns but retains exit 0 DevX tests/integration/test_install_conflicted_lockfile_e2e.py::test_dry_run_names_the_conflict_and_preserves_the_file component
4 Global commands diagnose user state, not the local project DevX tests/integration/test_install_conflicted_lockfile_e2e.py::test_global_commands_diagnose_only_the_user_lockfile component
5 Export from a subdirectory diagnoses current/legacy input without migration Governed by policy tests/integration/test_install_conflicted_lockfile_e2e.py::test_export_from_ancestor_preserves_the_conflicted_lockfile component
6 Restore known-good bytes and retry without a Git merge DevX tests/integration/test_install_conflicted_lockfile_e2e.py::test_manual_restore_allows_retry_without_a_git_merge component
7 Best-effort discovery remains best-effort DevX tests/integration/test_install_conflicted_lockfile_e2e.py::test_best_effort_installed_paths_preserves_conflicted_bytes component
8 Parser output is not exposed for conflicts; other invalid input stays unchanged Secure by default tests/unit/deps/test_lockfile_conflict_markers.py component

How to test

  • Run the targeted pytest command above; expect 114 passes and one existing skip.
  • In a disposable project, put two-way or diff3 markers in its lockfile.
    Run normal, frozen, partial and preview installs; expect named manual guidance,
    unchanged bytes, exit 1 for installs and exit 0 for preview.
  • Repeat with LF/CRLF, a spaced directory, an ancestor project and --global;
    confirm the diagnosed file is the one selected by that invocation.
  • Restore known-good bytes and retry the original command with its original
    options; confirm no active Git merge is required.
  • Run bash scripts/lint-architecture-boundaries.sh; expect no diagnostics.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Resolve the remaining lockfile path, decoding, frozen-guidance, and documentation issues.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds conflict-marker detection for lockfiles, regenerating them during full installs while failing closed for frozen and partial operations.

Changes:

  • Updates install, lock, MCP, dry-run, and frozen-mode handling.
  • Adds unit and integration coverage.
  • Updates CLI documentation and changelog.
File summaries
File Description
tests/unit/install/test_frozen.py Tests frozen conflict behavior.
tests/unit/deps/test_lockfile_conflict_markers.py Tests marker detection and discard behavior.
tests/integration/test_install_conflicted_lockfile_e2e.py Covers CLI recovery and failure modes.
src/apm_cli/install/service.py Adds frozen-mode failure handling.
src/apm_cli/install/presentation/dry_run.py Reports conflicts without modifying files.
src/apm_cli/install/mcp/command.py Surfaces MCP lockfile errors.
src/apm_cli/install/errors.py Refines frozen recovery guidance.
src/apm_cli/deps/lockfile.py Detects and discards conflicted lockfiles.
src/apm_cli/commands/lock.py Applies recovery and export handling.
src/apm_cli/commands/install.py Regenerates conflicted files during full installs.
docs/src/content/docs/troubleshooting/install-failures.md Documents recovery steps.
docs/src/content/docs/reference/lockfile-spec.md Documents conflict semantics.
docs/src/content/docs/reference/cli/lock.md Documents apm lock behavior.
docs/src/content/docs/reference/cli/install.md Documents install and frozen behavior.
CHANGELOG.md Records the fix.
Review details

Suppressed comments (7)

CHANGELOG.md:29

  • The #2979 suffix is the linked issue number, not this pull request's number. The changelog contract requires each entry to end with the actual PR number; replace this suffix with the PR number when it is known.
- A full `apm install` and `apm lock` now warn, discard `apm.lock.yaml`, and resolve from `apm.yml` when the lockfile still contains git merge conflict markers, instead of exiting with a YAML parse error. `apm install --frozen`, partial installs, and read-only commands such as `apm update` and `apm outdated` fail closed with an error that names the conflict and the next action, and the `--frozen` failure tip no longer points at commands that cannot read the lockfile. (#2979)

docs/src/content/docs/reference/cli/install.md:153

  • The CLI behavior changed here, but the maintained packages/apm-guide/.apm/skills/apm-usage/ resources were not updated: commands.md:15 still describes frozen mode only as missing/out-of-sync, and troubleshooting.md:85-89 has no merge-conflict recovery. Add the concise install/lock conflict behavior there so the package guidance does not give stale recovery instructions.
- **Frozen mode.** With `--frozen`, install resolves only what is in `apm.lock.yaml`. A missing lockfile, a direct dependency missing from it, or MCP config state that differs from `apm.yml` exits `1` before lockfile, target config, deployment, or cache mutation. Cold-cache installs (empty `apm_modules/`) with git `apm_package` deps are tolerated: MCP checks are skipped for absent package directories (the packages will be hydrated by the pipeline), and their MCP server configs are restored from the lockfile so no false drift is reported. Remote `claude_skill` dependencies declared at a repository root or subdirectory are also accepted from their locked type before materialization; once present, the lock type and detected skill shape must agree. Missing local paths still fail. A lockfile that contains git merge conflict markers also exits `1` and is never rewritten under `--frozen`. See [`config-consistency`](../../baseline-checks/#config-consistency) for the full manifest rule. Run normal `apm install` to create or repair MCP-only lock state, or to discard a conflicted lockfile and resolve from `apm.yml`, then retry frozen mode. Add-style invocations (`apm install PACKAGE` and `apm install --mcp NAME`) are rejected because they mutate `apm.yml`. Orphan package lock entries are tolerated; local-path deps are skipped. This is a structural check, not a content check -- run `apm audit --ci` for hash verification.

docs/src/content/docs/reference/lockfile-spec.md:398

  • This "Every command" claim is broader than the current behavior: commands/view.py::_lookup_lockfile_ref and commands/deps/cli.py catch Exception around LockFile.read and continue without lockfile metadata, so those readers still do not name this conflict. Narrow the sentence to commands that require the lockfile, or update those best-effort readers to surface the error.
conflict rather than a YAML error. Every command that reads the lockfile names
the file and the next action. A full `apm install` (no package arguments, no

src/apm_cli/commands/lock.py:313

  • apm lock export is a read-only lockfile consumer, but this new LockFile.read call still uses only get_lockfile_path. A project that has only the supported legacy apm.lock is therefore reported as having no lockfile, and a conflict in that file is never classified; route the path through resolve_lockfile_path_for_read(project_root, read_only=True) as the other read-only consumers do (for example, commands/outdated.py:449).
    lockfile = LockFile.read(lockfile_path)

src/apm_cli/deps/lockfile.py:1276

  • This discard probe performs a second unguarded UTF-8 decode. A non-UTF-8 lockfile reaches it before the pipeline's LockFile.read, so a full install reports a raw UnicodeDecodeError instead of the normalized fail-closed lockfile error. Catch and normalize the decode here, leaving the file in place so it cannot be discarded as conflicted.
    if not path.exists() or not has_conflict_markers(path.read_text(encoding="utf-8")):
        return False

src/apm_cli/install/errors.py:103

  • This new early return also applies to FrozenInstallError from the generic unreadable-lockfile path in InstallService.enforce_frozen: that message only says --frozen could not read ... and does not tell the user how to recover. Preserve actionable guidance for unreadable (non-missing) lockfiles, or make that exception message include the normal-install repair action while keeping the missing-file case free of the obsolete outdated tip.
    if not error.reasons:
        return ""

tests/integration/test_install_conflicted_lockfile_e2e.py:16

  • This new integration module invokes the Click CLI in-process via CliRunner and touches a temporary filesystem, so it needs the module-level component behavioral marker. Without pytestmark = pytest.mark.component, the new tests are left outside the repository's marker-only taxonomy and are not selected by component-scoped runs (see tests/quality/test_test_taxonomy.py:152-163).
import pytest
from click.testing import CliRunner
  • Files reviewed: 15/15 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/apm_cli/deps/lockfile.py Outdated
@sergio-sisternes-epam

Copy link
Copy Markdown
Collaborator

Thank you for contributing this pull request.

This PR is linked to #2979, which already carries maintainer status/accepted. Advisory triage recommendation is ready-for-review. That is not merge approval, not assignment, and not a request to run a review panel.

CODEOWNERS already requested danielmeppiel and sergio-sisternes-epam. This note does not add or change review requests.

A responsible human maintainer still needs to review the implementation against the accepted issue, including the chosen default: detect git conflict markers in the lockfile load owner, regenerate on a full non-frozen apm install / apm lock, and fail closed under --frozen and partial installs.


Generated by autopilot-pr-triage-worker. This comment is AI-generated and may contain errors.

@sergio-sisternes-epam Sergio Sisternes (sergio-sisternes-epam) added triage/recommended Automated advice completed; not human scope approval. type/bug Something does not work as documented. area/lockfile Lockfile schema, per-file provenance, integrity hashes, drift detection. area/cli CLI command surface, flags, help text (cross-cutting). theme/security Secure by default. Content scanning, lockfile integrity, MCP trust boundaries. labels Sep 20, 2026
@sergio-sisternes-epam Sergio Sisternes (sergio-sisternes-epam) added the status/accepted Human scope approval; verify the issue's approval record and review contact before work. label Sep 20, 2026
@sergio-sisternes-epam

Copy link
Copy Markdown
Collaborator

APM Review Panel: ship_with_followups

Conflicted lockfile recovery matches accepted #2979: detect in load, regenerate on full non-frozen install/lock, fail closed under --frozen and partial installs.

panel-mode=full; personas=python-architect,test-coverage-expert,doc-writer,performance-expert,cli-logging-expert,devx-ux-expert,supply-chain-security-expert

cc Lachlan Heywood (@lachieh) Daniel Meppiel (@danielmeppiel) Sergio Sisternes (@sergio-sisternes-epam) -- a fresh advisory pass is ready for your review.

This PR implements the accepted #2979 default without expanding product surface: conflict detection lives in LockFile.read, a full non-frozen install/lock regenerates via the existing missing-lock path, and --frozen plus partial installs stay fail-closed. Supply-chain, CLI logging, and performance agree the change is load-bearing and cheap: no integrity bypass, named next-action copy, and a single precompiled regex on small YAML.

Specialists converge on one real durability gap, not a security hole. discard_conflicted_lockfile unlinks apm.lock.yaml outside InstallTransaction, so a later install failure cannot restore the conflicted file through the transaction; the author documented that git checkout recovers it, and there is no test of unlink-then-fail. Architect's ask for one conflict-outcome owner is the same story as cleanup, not a competing design. Test-coverage's --force gap is a false product claim: --force is not a lock-regenerate switch and should stay unchanged. DevX CI autodetection and a dry-run machine-detectable exit contract would rewrite accepted scope; keep dry-run as warn + exit 0.

Docs should stay as precise as the code: full install/lock recover, frozen/partial fail closed, dry-run does not rewrite. Drop unrelated hermes/target churn in commands.md. CODEOWNERS should confirm the implementation against the accepted default; remaining work is follow-up, not a new policy.

Dissent. test-coverage-expert marked unlink-then-fail missing coverage as blocking and asked for a --force override test; python-architect rated the same unlink as recommended, and product intent is that --force is not a regenerate switch. I keep unlink-then-fail as the top follow-up (author-stated limitation, git checkout recovers) and drop the --force test. I also side against DevX CI-detect and dry-run exit-code expansion because they change accepted #2979.

Aligned with: Portable by manifest: a conflicted lock is treated as missing on full install/lock and rewritten from the manifest; frozen and partial paths refuse to proceed with a bad pin file. Secure by default: regeneration reuses the existing missing-lockfile path; --frozen and non-conflict corruption stay fail-closed. Supply-chain found no integrity bypass. Governed by policy: detect-in-load plus mode-based outcome (regenerate vs fail-closed) matches the accepted default; do not add a silent CI autodetection policy. OSS community-driven: external author on an accepted issue; keep review on the ratified default rather than extra flags that would move the contract. Pragmatic as npm: full install recovers a conflicted lock the way a missing lock already does; frozen/partial stay closed; --force is left alone.

Panel summary

Persona B R N Takeaway
Python Architect 0 2 1 Conflicted-lockfile detect is centralized, but discard-vs-fail-closed and the unlink still fork outside InstallTransaction.
CLI Logging Expert 0 0 0 Conflict named with next action; empty frozen tip is intentional so outdated/update are not suggested.
DevX UX Expert 0 2 1 Tighten regenerate vs fail-closed defaults, dry-run exit codes for CI, and non-transactional lockfile updates.
Supply Chain Security Expert 0 0 0 Conflicted-lock regen is the existing missing-lockfile path; --frozen and non-conflict corruption stay fail-closed. No integrity bypass.
Doc Writer 0 2 1 Conflict matrix is in the right pages; keep recover vs fail-closed explicit, canonicalize in lockfile-spec, and drop unrelated commands.md hermes churn.
Test Coverage Expert 1 2 0 PR adds focused unit + integration tests but misses failed-install-after-discard, --force, and lifecycle unlink-then-fail snapshot.
Performance Expert 0 0 0 Single multiline regex in LockFile.read on small YAML; no measurable perf impact; regex precompiled at module scope.

B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.

Top 5 follow-ups

  1. [Test Coverage Expert] (blocking-severity) Add an integration fixture for failed install after conflicted-lock unlink/discard. -- Author-stated limitation: discard is outside the transaction, so durable state after unlink-then-fail is unguarded. git checkout recovers; the gap is a regression trap, not a new product switch.
  2. [Python Architect] Move conflicted-lock discard into InstallTransaction so a failed install can restore the prior file. -- Unlink today happens before resolve; later failure cannot roll the conflicted lock back through the existing transaction owner.
  3. [Doc Writer] Keep recover vs fail-closed explicit; do not claim every install rewrites a conflicted lock. -- Full install/lock recover; frozen/partial fail closed; dry-run warns and does not rewrite. Over-claim would fight the accepted default.
  4. [Doc Writer] Revert unrelated hermes/target wording churn in packages/apm-guide/.apm/skills/apm-usage/commands.md. -- A lockfile-conflict patch should not rewrite the hermes/target table; duplicate install rows already disagree.
  5. [Python Architect] Collapse discard vs fail-closed vs warn into one lockfile conflict policy used by install, lock, frozen, dry-run, and MCP. -- Detection is already centralized in LockFile.read; durable outcome is still recomputed at each call site.

Architecture

classDiagram
    direction LR
    class LockFile {
      <<ValueObject>>
      +read(path) LockFile
    }
    class has_conflict_markers {
      <<Pure>>
    }
    class LockfileFormatError {
      <<DomainError>>
    }
    class LockfileConflictError {
      <<DomainError>>
      +path Path
    }
    class discard_conflicted_lockfile {
      <<IOBoundary>>
    }
    class InstallTransaction {
      <<UnitOfWork>>
      +commit(result) InstallResult
      +fail(error) InstallResult
    }
    class InstallService {
      +LockFile.read for frozen
    }
    class FrozenInstallError {
      <<DomainError>>
    }
    LockfileConflictError --|> LockfileFormatError
    LockFile ..> has_conflict_markers : detect
    discard_conflicted_lockfile ..> has_conflict_markers : detect
    LockFile ..> LockfileConflictError : raises
    InstallService ..> LockFile : reads
    InstallService ..> FrozenInstallError : raises
    note for LockFile "Canonical detect: LockFile.read raises LockfileConflictError"
    note for discard_conflicted_lockfile "Outcome fork: unlink on full install and apm lock"
    note for InstallTransaction "Durable lockfile mutation belongs in this unit of work"
    class LockFile:::touched
    class LockfileConflictError:::touched
    class discard_conflicted_lockfile:::touched
    class InstallService:::touched
    classDef touched fill:#fff3b0,stroke:#d47600
Loading
flowchart TD
    installEntry["commands/install.py:install"] --> packages["_install_apm_packages"]
    packages --> migrate["[FS] migrate_lockfile_if_needed"]
    migrate --> fullGate{"full_install: not frozen and not packages and InstallMode.ALL"}
    fullGate -->|yes| discard["[FS] lockfile.py:discard_conflicted_lockfile path.unlink"]
    fullGate -->|no| laterRead["[I/O] LockFile.read"]
    discard --> resolve["_install_apm_dependencies"]
    laterRead --> conflict{"LockfileConflictError?"}
    conflict -->|frozen InstallService| frozenErr["raise FrozenInstallError"]
    conflict -->|partial add or --only| failClosed["fail closed; lockfile left in place"]
    frozenErr --> txnFail["InstallTransaction.fail"]
    lockEntry["commands/lock.py:_run_lock"] --> lockDiscard["[FS] discard_conflicted_lockfile"]
    lockDiscard --> lockResolve["_install_apm_dependencies"]
    mcpEntry["install/mcp/command.py:run_mcp_install"] --> mcpWrite["[FS] add_mcp_to_apm_yml"]
    mcpWrite --> mcpRead["[I/O] LockFile.read during integration"]
    mcpRead -->|LockfileFormatError| mcpClick["raise click.ClickException; apm.yml already written"]
    dryEntry["presentation/dry_run.py:render_and_exit"] --> dryRead["[I/O] LockFile.read"]
    dryRead -->|LockfileConflictError| dryWarn["logger.warning; treat lock as missing"]
Loading

Recommendation

CODEOWNERS should confirm detect-in-load, regenerate on full non-frozen install/lock, and fail-closed under --frozen and partial installs. Fold the docs precision pass (no every-install recover claim; revert commands.md hermes churn) if it is still cheap in this PR. Track unlink-then-fail coverage and transactional discard as follow-ups; leave --force, CI autodetection, and dry-run exit codes out of scope.


Full per-persona findings

Python Architect

  • [recommended] Conflicted-lockfile outcome is split across call sites instead of one owner. at src/apm_cli/commands/install.py:1837
    Detection is centralized in LockFile.read; durable outcome (unlink vs fail closed vs warn) is recomputed at install, lock, frozen, dry-run, and MCP.
    Suggested: One lockfile conflict policy invoked from those call sites.
  • [recommended] discard_conflicted_lockfile unlinks apm.lock.yaml outside InstallTransaction. at src/apm_cli/deps/lockfile.py:1283
    unlink before resolve; later install failure cannot restore the conflicted file via InstallTransaction.
  • [nit] MCP install catches LockfileFormatError after writing apm.yml. at src/apm_cli/install/mcp/command.py:308
    Pre-existing order; conflicted lock can leave manifest write without regenerated lock.

CLI Logging Expert

No findings.

DevX UX Expert

  • [recommended] Make regenerate vs fail-closed explicit for interactive vs CI
    The PR implements both regenerate-on-full-install and fail-closed for frozen/partial installs. Users and CI need a clear rule about which behavior is the default. CEO: accepted scope already covers this; do not add CI autodetection.
  • [recommended] Clarify dry-run output and ensure machine-detectable exit codes
    Dry-run currently reports human-readable outcomes like "would make no changes" vs warnings. Author documented this limitation. CEO: keep warn + exit 0.
  • [nit] Address non-transactional lockfile/regeneration risks in UX and docs
    Regenerating without atomic replace risks partial state if interrupted.

Supply Chain Security Expert

No findings.

Doc Writer

  • [recommended] Do not over-claim that every install recovers a conflicted lockfile at docs/src/content/docs/reference/cli/install.md
    Full install/lock recover; frozen/partial fail closed; dry-run does not rewrite.
  • [recommended] Revert unrelated hermes/target wording churn in commands.md at packages/apm-guide/.apm/skills/apm-usage/commands.md:15
    A lockfile-conflict patch should not rewrite the hermes/target table; duplicate install rows already disagree.
  • [nit] State conflict semantics once; point other pages at lockfile-spec at docs/src/content/docs/reference/lockfile-spec.md
    Canonical definition belongs in lockfile-spec; recovery steps in install-failures.md.

Test Coverage Expert

  • [blocking] No test that a failed install after discard/unlink leaves durable state safe
    Author-stated limitation: discard outside transaction.
    Proof (missing at): tests/integration/test_install_failed_after_discard_unlink.py
  • [recommended] No explicit --force override test
    Product intent: --force is unchanged and is NOT a lock regenerate switch. CEO dropped this follow-up.
    Proof (missing at): tests/integration/test_install_force_override.py
  • [recommended] No ApmLifecycle snapshot for unlink-then-fail
    Overlaps the unlink-then-fail coverage gap.
    Proof (missing at): tests/integration/test_lifecycle_unlink_then_fail_snapshot.py

Performance Expert

No findings.

This panel is advisory. It does not block merge. Re-apply the
panel-review label after addressing feedback to re-run.


Generated by autopilot-pr-review-worker. This comment is AI-generated and may contain errors.

@sergio-sisternes-epam

Copy link
Copy Markdown
Collaborator

Thank you Lachlan Heywood (@lachieh)

I have enabled the merge queue for this PR. Please review the blocker actions from the APM Review Panel. Once the test coverage is fixed.

Optionally, if you can take out the top 5 recommendations, that could help us reduce the technical debt.

Thank you for your contribution.

Sergio

@lachieh

Lachlan Heywood (lachieh) commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks Sergio Sisternes (@sergio-sisternes-epam). The follow-ups are addressed in #3043. I left this PR as is so the merge queue can take it.

I tried to stack on this branch, but cross-fork PRs can't target fork branches as the base so that branch will show 2 commits until this one merges.

Lachlan Heywood (lachieh) added a commit to lachieh/apm that referenced this pull request Sep 20, 2026
InstallTransaction now owns the conflicted-lockfile discard: it snapshots
the bytes before unlinking and rollback puts the file back unless the
attempt already wrote a new lockfile. apm lock runs under its own
transaction so the same rule applies there. The module-level
discard_conflicted_lockfile helper is removed.

Follow-up to microsoft#3028 from the APM Review Panel.
Lachlan Heywood (lachieh) added a commit to lachieh/apm that referenced this pull request Sep 20, 2026
Other lockfile format errors keep the redacted, verbose-only handling
that path had before microsoft#3028 widened the except clause.
auto-merge was automatically disabled September 20, 2026 23:22

Head branch was pushed to by a user without write access

Lachlan Heywood (lachieh) added a commit to lachieh/apm that referenced this pull request Sep 20, 2026
InstallTransaction now owns the conflicted-lockfile discard: it snapshots
the bytes before unlinking and rollback puts the file back unless the
attempt already wrote a new lockfile. apm lock runs under its own
transaction so the same rule applies there. The module-level
discard_conflicted_lockfile helper is removed.

Follow-up to microsoft#3028 from the APM Review Panel.
Lachlan Heywood (lachieh) added a commit to lachieh/apm that referenced this pull request Sep 20, 2026
Other lockfile format errors keep the redacted, verbose-only handling
that path had before microsoft#3028 widened the except clause.
Lachlan Heywood (lachieh) added a commit to lachieh/apm that referenced this pull request Sep 20, 2026
InstallTransaction now owns the conflicted-lockfile discard: it snapshots
the bytes before unlinking and rollback puts the file back unless the
attempt already wrote a new lockfile. apm lock runs under its own
transaction so the same rule applies there. The module-level
discard_conflicted_lockfile helper is removed.

Follow-up to microsoft#3028 from the APM Review Panel.
@lachieh

Copy link
Copy Markdown
Contributor Author

Rebased onto main (CHANGELOG moved under the post-0.31.0 Unreleased section) and added 4456bb6b: the MCP-add path now names only LockfileConflictError at default verbosity, so other lockfile format errors keep their redacted, verbose-only handling. The panel's blocker (unlink-then-fail coverage, with transactional restore) is on the follow-up branch and #3043 will be reopened right after this merges, so its review shows only that diff.

Lachlan Heywood (lachieh) added a commit to lachieh/apm that referenced this pull request Sep 22, 2026
InstallTransaction now owns the conflicted-lockfile discard: it snapshots
the bytes before unlinking and rollback puts the file back unless the
attempt already wrote a new lockfile. apm lock runs under its own
transaction so the same rule applies there. The module-level
discard_conflicted_lockfile helper is removed.

Follow-up to microsoft#3028 from the APM Review Panel.
@lachieh

Lachlan Heywood (lachieh) commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor Author

Sergio Sisternes (@sergio-sisternes-epam) the APM Review Panel's blocking item is now fixed in this PR rather than a stacked follow-up, so there is nothing left to merge separately.

168fcb1 moves the conflicted-lockfile discard into InstallTransaction: it snapshots the bytes before unlinking and rollback() restores the file unless the attempt already wrote a new one. That closes follow-ups 1 and 2 together.

Follow-up 3 (docs precision) is in the docs commits.

On follow-up 4, the commands.md diff here is only the --frozen phrase.

a9f5590 registers the decision as an architecture owner which is follow-up 5.

Per the panel's own dissent I left --force, CI autodetection, and dry-run exit codes out of scope.

Lachlan Heywood (lachieh) added a commit to lachieh/apm that referenced this pull request Sep 23, 2026
Other lockfile format errors keep the redacted, verbose-only handling
that path had before microsoft#3028 widened the except clause.
Lachlan Heywood (lachieh) added a commit to lachieh/apm that referenced this pull request Sep 23, 2026
InstallTransaction now owns the conflicted-lockfile discard: it snapshots
the bytes before unlinking and rollback puts the file back unless the
attempt already wrote a new lockfile. apm lock runs under its own
transaction so the same rule applies there. The module-level
discard_conflicted_lockfile helper is removed.

Follow-up to microsoft#3028 from the APM Review Panel.
@sergio-sisternes-epam

Copy link
Copy Markdown
Collaborator

Lachlan Heywood (@lachieh) Spec Conformance CI check failed. Please submit a quick fix so I can approve and close the PR. Merge queue is engaged, by your change will require a new approval from my side.

Thank you!

Lachlan Heywood (lachieh) added a commit to lachieh/apm that referenced this pull request Sep 23, 2026
Other lockfile format errors keep the redacted, verbose-only handling
that path had before microsoft#3028 widened the except clause.
Lachlan Heywood (lachieh) added a commit to lachieh/apm that referenced this pull request Sep 23, 2026
InstallTransaction now owns the conflicted-lockfile discard: it snapshots
the bytes before unlinking and rollback puts the file back unless the
attempt already wrote a new lockfile. apm lock runs under its own
transaction so the same rule applies there. The module-level
discard_conflicted_lockfile helper is removed.

Follow-up to microsoft#3028 from the APM Review Panel.
auto-merge was automatically disabled September 23, 2026 20:57

Head branch was pushed to by a user without write access

@lachieh

Copy link
Copy Markdown
Contributor Author

Sergio Sisternes (@sergio-sisternes-epam) fixed in c3e534ee.

The gate was the Mode B silent-extension detector, not a test failure. A waiver would have been the wrong call: conflicted-lockfile recovery is observable behaviour under critical paths, so my earlier "N/A -- does not change OpenAPM-observable behaviour" was wrong. Added the citation instead.

req-lk-023 (Section 5.4, consumer MUST), three clauses matching what this PR implements:

  1. detection is centralised at lockfile load, and the diagnostic names the path plus a next action executable while the markers are present -- which rules out the original bug, where --frozen pointed at apm outdated/apm update and neither could read the file;
  2. only an operation that re-resolves every declared dependency may discard the lockfile; replay, subset-scoped, and preview operations fail closed without removing or rewriting it;
  3. a discard is reverted when the operation writes no replacement, and a replacement written by the operation supersedes the discarded file.

A lockfile unreadable for any other reason is explicitly out of scope and keeps failing closed.

Full ritual in the same commit: anchor + prose, Appendix C row, Section 5.7 and 11.3.2 enumerations, statement counts (123 -> 124, 119 MUST), revision history 0.1.42, the manifest entry, three @pytest.mark.req("req-lk-023") oracles (one per clause) in tests/spec_conformance/test_lockfile_reqs.py, and regenerated CONFORMANCE.{md,json}.

Locally: orphan_check reports 124 requirements aligned across anchors / manifest / Appendix C / pytest markers, mode_b_detector.sh passes, and tests/spec_conformance + tests/quality + the install/lockfile suites are green (4525 passed). Also rebased onto current main.

Understood that this needs a fresh approval from you.

@danielmeppiel Daniel Meppiel (danielmeppiel) changed the title fix(install): discard lockfile with git conflict markers, fail closed under --frozen fix(lockfile): diagnose merge conflicts without automatic recovery Sep 24, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lachlan Heywood (@lachieh), thank you for the investigation and the work on the recovery safeguards. I am narrowing the scope of this PR: ship the diagnostic bug fixes here; defer automatic recovery as a separate feature/design decision.

The approved diagnostic-only scope is recorded on #2979:
#2979 (comment)

Please revise this PR as follows:

  1. Keep conflict detection and clear errors. Use the existing canonical lockfile-load owner to identify Git conflict markers and name the affected file and cause. Preserve existing best-effort readers and redaction; do not expose unrelated raw parser content.
  2. Fix the unusable repair advice. For an unreadable/conflicted lockfile, tell the user to resolve the conflict or restore a known-good lockfile before retrying. Do not recommend apm outdated, apm update, or a non-frozen install as though those commands can repair the same unreadable file.
  3. Remove automatic recovery from this PR. Full install and apm lock must not discard or regenerate the conflicted file. Frozen, partial and preview paths must also preserve it. Remove recovery-only discard/restore machinery, its newly introduced architecture-owner/guard additions, and tests that exist solely to authorize that recovery; preserve unrelated existing transaction behavior and guards.
  4. Keep the specification honest and equally narrow. Remove recovery-authorizing clauses from the proposed req-lk-023 change and corresponding recovery-only manifest, count and generated conformance changes. Retain applicable diagnostic conformance evidence under existing requirements where appropriate. If the diagnostic-only change genuinely needs a new normative amendment, bring that narrow amendment back for agreement rather than retaining the automatic-recovery contract or bypassing the conformance check with a waiver.
  5. Prove and document the diagnostic-only contract. Cover conflict recognition without false positives, actionable messages, byte-for-byte lockfile preservation across the affected commands, unchanged handling of other corruption, and the preserved offline/frozen/security/exit behavior. Update the relevant docs, apm-guide resources, changelog and PR description; the current implementation/test prose still describes the wider draft.

The original automatic-recovery request remains open as a deferred type/feature requiring design. This PR remains an accepted type/bug only for the bounded diagnostic slice. I changed its closing reference to an ordinary issue reference so it will not close #2979.

For the future design discussion, npm's conflict-aware JSON merging is not equivalent to throwing away APM's lockfile and resolving again, particularly when exact pins and deployment records can be lost. No automatic merging, regeneration, new flag, or change to --force is approved here.

Sergio Sisternes (@sergio-sisternes-epam), this is a new maintainer scope decision, not a claim that the contributor failed to follow the earlier review. Please preserve the existing review history; the revised diagnostic-only head will need renewed human review. I am requesting changes on the current head, not authorizing a merge or launching another implementation/review worker.

@lachieh
Lachlan Heywood (lachieh) force-pushed the install-on-invalid-lockfile branch 5 times, most recently from a853b0b to c19ca66 Compare September 25, 2026 16:17
@lachieh

Copy link
Copy Markdown
Contributor Author

Thanks, Daniel Meppiel (@danielmeppiel). Narrowed to the diagnostic slice. The conflicted lockfile is now preserved byte-for-byte on install, lock, frozen, partial and preview paths.

One slight modification; Since it is going to be agents that are likely reading this message and resolving the issue the message names the repair as commands rather than just leaving resolution as a guess.

apm.lock.yaml contains unresolved git merge conflict markers.
Keep one side of the merge, then reinstall:
    git checkout apm.lock.yaml --ours # or --theirs
    apm install

Keeping a side preserves that branch's pins; the install reconciles only what the merge added. The git command resolves the file before any apm command runs, so nothing points at the unreadable lockfile. Tests run the printed commands against a real merge conflict rather than matching their wording.

  1. Keep the specification honest and equally narrow. Remove recovery-authorizing clauses from the proposed req-lk-023 change and corresponding recovery-only manifest, count and generated conformance changes. Retain applicable diagnostic conformance evidence under existing requirements where appropriate. If the diagnostic-only change genuinely needs a new normative amendment, bring that narrow amendment back for agreement rather than retaining the automatic-recovery contract or bypassing the conformance check with a waiver.

Regarding this point; the diagnostic-only diff still trips Mode B (61 lines, threshold 20), and no existing requirement covers lockfile-load diagnostics; req-lk-004 is specific to an unrecognised lockfile_version. With the waiver ruled out, and pending your agreement, req-lk-023 as it is on the current commit is that narrow amendment. Clause (c) encodes your request, but it does also make the deferred recovery feature a spec amendment rather than a feature decision. I'm happy to cut it back to just recognition and message if you'd prefer.

@danielmeppiel

Copy link
Copy Markdown
Collaborator

APM Review Panel: ship_with_followups

Lockfile merge-conflict diagnosis is architecturally sound; printed git recipe does not reliably deliver an actionable next step for all reader paths

panel-mode=lean; personas=devx-ux-expert,python-architect,test-coverage-expert,apm-ceo

cc Lachlan Heywood (@lachieh) -- a fresh advisory pass is ready for your review.

Compared with the September 20 panel at 4ea7c9a3, this head removes automatic recovery and its discard/transaction machinery. That older recovery advice is not repeated here. The current diff centralizes marker detection in LockFile.read and keeps existing failure/best-effort paths; the proposed req-lk-023 amendment is evaluated separately and remains subject to human agreement.

The remaining concern is the printed next step. For committed or copied markers, git checkout --ours is not guaranteed to repair the file; it may restore the same markers. For apm outdated --global or apm lock export from a subdirectory, the error names the actual lockfile but prints only its basename, so the command can fail or target a different local file. These gaps bear directly on the accepted promise of actionable manual resolve/restore guidance. The frozen handler also duplicates the canonical diagnostic, a smaller maintenance concern.

Limitations: the test-coverage task returned no usable response, so this run provides no independent test-coverage conclusion. No contribution code or tests were executed. GitHub reports successful Lint, Spec conformance gate and test-shard check-runs at this exact head; those are observed statuses, not local validation. The supplemental reviewer request using @me failed because GitHub could not resolve that login; existing review ownership was left unchanged.

Dissent. No disagreement among the returning code panelists on severity. The missing test return is a review limitation, not evidence of adequate coverage.

Aligned with: canonical lockfile loading and preserved recorded state support reproducibility; the repair message still needs to match the file and context the user actually has.

Panel summary

Persona B R N Takeaway
Python Architect 0 1 0 Frozen conflict handler forks canonical LockfileConflictError diagnostic; otherwise centralization to LockFile.read is architecturally sound.
DevX UX Expert 0 2 0 Git-only advice has applicability and path-targeting gaps; keep manual resolve/restore guidance tied to the diagnosed file.
Test Coverage Expert n/a n/a n/a Review unavailable: the synchronous task returned no JSON; no test-coverage conclusion or executed-test evidence is claimed.

B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships. n/a denotes an unavailable review.

Top 3 follow-ups

  1. [DevX UX Expert] Add manual-edit or restore-known-good fallback for scenarios where git checkout --ours lacks a guaranteed repair path (committed markers, non-merge state). -- The human done-when requires an actionable next step for all readers. Committed or copied marker content triggers detection but the git-only recipe may restore the same markers without providing a working alternative.
  2. [DevX UX Expert] Use the diagnosed actual path rather than basename-only path.name for global and ancestor-export lockfile reads. -- Global (~/.apm) and ancestor-export lockfile paths differ from the caller working directory; the basename-relative git command may target the wrong file or fail outright.
  3. [Python Architect] Compose the frozen-specific framing around the canonical LockfileConflictError message rather than reconstructing it independently. -- Two message authorities for the same diagnostic risk silent divergence when the canonical wording or path derivation changes.

Recommendation

The diagnosis is correct and architecturally sound. The implementation targets the primary project-root merge-conflict scenario; that recipe was not executed in this review. However, the first two follow-ups above address scenarios where the printed recipe does not deliver the actionable next step the human scope requires -- they are not deferred polish. The maintainer should weigh whether those gaps are acceptable for the initial landing or warrant a pre-merge iteration. Human review and spec agreement remain pending independently of this advisory.


Full per-persona findings

Python Architect

  • [recommended] Frozen handler in install/service.py hardcodes apm.lock.yaml and reconstructs the conflict diagnostic independently of LockfileConflictError, creating a second message authority. at src/apm_cli/install/service.py
    Single-owner rule (architecture.instructions.md): every durable decision has exactly one canonical owner; every call site routes through it. LockfileConflictError is the canonical owner of conflict diagnostic wording, path, and git-command format. The frozen handler at service.py:268-307 catches the typed error but discards its message, hardcodes the filename as 'apm.lock.yaml' instead of reading exc.path, and reconstructs the full prose ('--frozen cannot read apm.lock.yaml: it contains...') plus git commands independently. If the canonical error's wording, path.name derivation, or git command format changes, this handler silently diverges. Two authorities for one message is the pattern the single-owner rule exists to prevent. The frozen-specific prefix ('--frozen cannot read') and suffix ('then commit the result') are legitimate additions; they should compose around the canonical message, not replace it. This is not blocking because the hardcoded path is currently always correct for frozen installs (project_dir/apm.lock.yaml is the canonical lockfile location) and no correctness regression exists today -- the risk is silent divergence under future maintenance.

Design patterns -- Used in this PR: Base class + subclass (LockfileConflictError extending LockfileFormatError) for exception hierarchy; existing catch chains naturally specialize via ordered except clauses. Pragmatic suggestion: none -- the current shape is the simplest correct design at this scope; the fix is to compose the frozen prefix/suffix around str(exc) or exc.path, not to add a new pattern.
Suggested: Compose the frozen-specific framing around the canonical error rather than reconstructing it: use exc.path.name for the filename and reference str(exc) or the error's core message for the conflict diagnostic, adding only the frozen-specific prefix and 'then commit the result' suffix.

DevX UX Expert

  • [recommended] The Git-only recipe does not cover every file the marker detector diagnoses. at src/apm_cli/deps/lockfile.py:84
    Detection examines bytes, not Git index state. A copied/untracked conflict or a file whose committed contents already contain markers may have no usable merge side; checkout can fail or restore the same unreadable contents. The diagnostic supplies no manual-resolve or restore-known-good fallback. The current code was read, not executed in this review.
    Suggested: Lead with resolving the named file or restoring a known-good copy. Keep any Git-side selection as an optional, context-qualified example, then retry only after the file is readable.

  • [recommended] The printed basename can refer to a different lockfile than the diagnosed path. at src/apm_cli/deps/lockfile.py:89
    Lock export may find an ancestor manifest and outdated --global reads the user-scope lockfile without changing directory. The error names that actual path but prints git checkout apm.lock.yaml relative to the caller's directory. That recipe can fail or act on a different local file. An ancestor project may itself be a Git repository; the issue is path targeting, not an assumption that it is outside Git.
    Suggested: Keep manual resolve/restore guidance tied to the actual diagnosed path; if showing a Git example, explicitly qualify the path and working directory.

Test Coverage Expert

Review unavailable: the synchronous task completed without a JSON return. No coverage or executed-test claim is inferred.

This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.


Generated by autopilot-pr-review-worker. This comment is AI-generated and may contain errors.

@danielmeppiel

Copy link
Copy Markdown
Collaborator

APM Spec Guardian: fold_and_ship

Scope: editorial-patch; diff = +34/-3 lines across 1 file(s). Shocked-meter avg: 7.75/10.

All four panels converge on ship_with_followups with zero blocking findings and a shocked_meter average of 7.75. The fold-now list is three surgical edits -- an interim illustrative marker parenthetical (F1), a stale section heading (F2), and a BCP 14 keyword alignment (F3) -- that a single drafter pass can apply mechanically. The highest-signal deferred item is the normative minimum marker grammar (F4), which requires a maintainer decision on the recognizable pattern boundaries before it can be codified; the interim parenthetical in F1 provides implementer guidance while that decision is pending. Automatic recovery from conflicted lockfiles remains deferred and tracked on issue #2979 with no version commitment. This synthesis is advisory; specification ratification remains pending human review by the specification owner.

The following are recommendations only; no folds were applied. The code panel is separate. This review does not accept the proposed amendment, change current human review requirements, or close #2979.

Convergence

Panel Stance Shocked New B New R New N
Swagger / OpenAPI Editor ship_with_followups 8/10 0 1 2
OCI Distribution Editor ship_with_followups 8/10 0 1 1
Package-Manager Editor ship_with_followups 7/10 0 1 2
TAG Architect ship_with_followups 8/10 0 2 1

B = new blocking findings, R = new recommended, N = new nits.
Counts are raw reviewer signals, not gates; the package-manager nit count includes one item not carried forward after source verification. The maintainer ships.

Convergent themes (flagged by 2+ panels)

  • T1 -- Marker grammar underspecified: normative text references conflict markers without enumerating a minimum recognizable pattern set for interoperable conformance testing (supporting: sw-rec-r1-1, oci-rec-r1-1, pkg-rec-r1-1, tag-rec-r1-1)
  • T2 -- Section 5.4 heading stale: title reads versions and bumping rules but now also houses conflict-marker load-time validation (req-lk-023) (supporting: sw-nit-r1-2, oci-nit-r1-1, pkg-nit-r1-1, tag-nit-r1-1)

Fold now (3 item(s))

  1. [F1 / T1] req-lk-023 clause (a) -- After 'unresolved version-control merge conflict markers' in clause (a), insert an illustrative parenthetical: '(for example, lines beginning with seven consecutive less-than, greater-than, or pipe characters followed by a space or end-of-line)'. Immediately following the parenthetical, add a non-normative note: 'Note -- This parenthetical is an interim illustrative aid; the normative minimum marker grammar is not yet pinned by this specification.' This fold does NOT resolve the normative ambiguity and does NOT establish interoperable minimum grammar; it provides a concrete interim reference for implementers while the grammar decision (F4) remains open.
    Success criterion: grep req-lk-023 clause (a) for the parenthetical text and for the 'interim illustrative aid' label; confirm no new req-XXX anchor or BCP 14 keyword is introduced by the addition.

  2. [F2 / T2] Section 5.4 heading -- Replace the Section 5.4 heading 'Lockfile versions (1, 2) and bumping rules' with 'Lockfile versions, bumping rules, and load-time validation'. Verify Appendix C table rows referencing section 5.4 cite the section number (not title text) and require no update.
    Success criterion: grep for the new heading text in the spec body; verify the old heading no longer appears; confirm Appendix C row for req-lk-023 still cites section 5.4.

  3. [F3 / standalone] req-lk-023 clause (b) -- In the tail of clause (b), replace 'naming one as a follow-up step, sequenced after the resolving action, is permitted.' with 'naming one as a follow-up step, sequenced after the resolving action, MAY be included in the diagnostic.' This editorial substitution formalizes an already-granted permission using BCP 14 vocabulary per Section 2 conventions. It does NOT add a new normative statement, does NOT create a new req-XXX anchor, and does NOT increment the statement count; the MAY operates within the existing scope of req-lk-023.
    Success criterion: grep req-lk-023 clause (b) for 'MAY be included in the diagnostic'; confirm statement count in Section 1.3, Appendix C trailer, and Appendix D 0.1.42 row remains 124 (119 MUST, 5 SHOULD).

Defer to v0.1.1

  • [F4 / T1] req-lk-023 clause (a) or new Section 3 terminology row -- After the maintainer confirms the explicit recognizable minimum marker grammar -- character count (7-character fixed or flexible), line-start anchoring, space-or-EOL delimiter, bare-separator exclusion, and literal-scalar false-positive handling -- record that definition either as a Terminology row in Section 3 or as normative prose within req-lk-023. This replaces the interim illustrative parenthetical from F1 with the binding grammar.

  • [F5 / standalone] req-lk-023 clause (a) -- Evaluate whether 'through the same authority that loads the lockfile' should be relaxed to a temporal-behavioral constraint such as 'before or during the load that would otherwise attempt to parse the lockfile'. The current phrasing reflects an intentional single-authority design choice by the specification owner; any relaxation should be weighed against cross-implementation feedback and the observable-versus-architectural constraint distinction. This is a proposed refinement, not a defect.

Findings not carried forward

  • pkg-nit-r1-2 -- False premise. The Appendix D 0.1.42 row already uses the normative abstraction ('directs the user to resolve the conflict or restore a known-good lockfile, never to another operation that reads the same unreadable file'). The alleged git-command phrasing ('name the git command that keeps one side of the merge') does not appear in the specification row; it originates from companion documentation (lockfile-spec.md), which is non-normative. No change to the already-correct Appendix D row is warranted.

Linter notes (1 advisory check failed; 1 mixed-scope note)

  • [10] The Unreleased changelog entry does not mention the spec file path. The checklist treats that as an advisory SHOULD, not a correctness failure.
  • [11] Twelve Python files also changed; this is why the separate general panel applies. Its test reviewer was unavailable. No contribution code/tests or Python lint chain were run locally.
  • [1-7, 9] Mechanical checks passed: ASCII, banned-affiliation scan, five Appendix-A-referenced schema checks, 17 fixture parses, 124 unique requirement anchors, matching counts, internal links and fixture citations.
  • [8] Skipped as not applicable: zero Mermaid blocks.

Note: the synthesizer recommends folding the suggestions; the changelog note is worth addressing in that same pass if the maintainer accepts the amendment.

Linter handoff: F1's quoted insertion phrase is in the req-lk-023 preamble, not clause (a); use that actual location. A heading rename in F2 also changes its generated slug, so check all affected links. After any accepted edits, recheck 124 requirement anchors/count sites, section links, ASCII and affiliation-language restrictions. These results describe the current head only, not unperformed folds or a full CI certification.


Full per-panel findings

Swagger / OpenAPI Editor -- shocked_meter 8/10, confidence high

Summary: Clean single-requirement editorial-patch amendment. The normative text is internally consistent, counts match across all three locations, conformance class is correct, and the defensive non-authorization clause for automatic recovery is well-crafted. One substantive gap: the MUST-level obligation references an undefined term ('version-control merge conflict markers') that should be pinned to an observable byte pattern for interface-contract interoperability. Two editorial nits on keyword formality and section-heading staleness. No blocking findings.

New recommended findings (1)

  • [sw-rec-r1-1] req-lk-023 / Section 3 -- req-lk-023 binds a MUST to 'version-control merge conflict markers' but the term is undefined in Section 3 (Terminology) or inline. Two conformant implementations could disagree on which byte patterns constitute a marker (e.g. diff3-style angle-bracket lines vs. Perforce-style markers). The non-normative companion lockfile-spec.md pins specific Git patterns (seven angle-brackets / pipes at line start), but the normative spec does not. For an interface contract this is an interoperability gap: clause (a) says MUST identify 'the markers' but does not define what to identify.
    Recommended fix: Add either (i) a Terminology row in Section 3 defining 'merge conflict marker' as any line beginning with seven consecutive angle-bracket or pipe characters followed by a space or end-of-line (the diff3 convention shared by Git, Mercurial, and SVN), or (ii) a parenthetical in the req-lk-023 preamble: '...carrying unresolved version-control merge conflict markers (lines beginning with seven left-angle-brackets, seven right-angle-brackets, or seven pipe characters, each followed by a space or end-of-line)'. Either approach pins the MUST to an observable pattern without over-fitting to a single VCS.

New nit findings (2)

  • [sw-nit-r1-1] req-lk-023 clause (b) -- The tail of clause (b) reads 'naming one as a follow-up step, sequenced after the resolving action, is permitted.' The word 'permitted' functions as a normative grant but is not a BCP 14 keyword. Section 2 says lowercase variants carry no normative weight, so the sentence is technically non-normative -- yet it clearly intends to grant permission. Strict RFC 2119 / 8174 discipline would use MAY.
    Recommended fix: Rewrite the sentence: '...naming one as a follow-up step, sequenced after the resolving action, MAY be included in the diagnostic.'
  • [sw-nit-r1-2] Section 5.4 heading -- The heading reads 'Lockfile versions (1, 2) and bumping rules' but now also houses req-lk-023 (conflict-marker recognition), which is not a versioning or bumping concern. The heading text is stale relative to the broadened section scope.
    Recommended fix: Broaden the heading to reflect all load-time gatekeeping, e.g. 'Lockfile versions, bumping rules, and load-time validation', or split req-lk-023 into its own subsection (5.4.1).

Preserved strengths confirmed

  • Mechanical checks independently found 124 unique normative anchors and matching Section 1.3, Appendix C and latest Appendix D totals.
  • Consumer MUST enumeration and explicit exclusion of automatic recovery are preserved.

OCI Distribution Editor -- shocked_meter 8/10, confidence high

Summary: The diagnosis-only amendment preserves the file and avoids raw parser output. The marker pattern set needs precision for consistent diagnostics; no security bypass was demonstrated.

New recommended findings (1)

  • [oci-rec-r1-1] req-lk-023 -- The MUST refers to unresolved version-control merge conflict markers without defining recognizable byte patterns. The implementation excludes a bare ======= separator and scans opener, closer and diff3 forms, but this choice is not explicit in the normative text. Implementations can therefore disagree about which inputs receive the named conflict diagnosis. No security bypass was demonstrated.
    Recommended fix: Add a non-normative note immediately after the closing paragraph of req-lk-023, e.g.: 'Note -- For the purposes of this requirement, the canonical conflict marker set is the sequences <<<<<<< , >>>>>>> , and ||||||| appearing at the start of a line, each followed by a space or end-of-line. A bare ======= separator alone is not sufficient evidence of a conflict because a real version-control conflict always includes at least one labelled marker.' This keeps the MUST portable across VCS tooling while giving implementers a shared reference set for conformance testing.

New nit findings (1)

  • [oci-nit-r1-1] sec.5.4 -- req-lk-023 is placed in Section 5.4 whose title is 'Lockfile versions (1, 2) and bumping rules'. Conflict-marker detection is a load-time validation concern, not a versioning or bumping concern. The Appendix C index correctly lists the section as 5.4, so a reader scanning by section title would miss the requirement.
    Recommended fix: No immediate action required; note for the next editorial pass that Section 5.4 could be retitled to 'Lockfile versions, load-time validation, and bumping rules' or that req-lk-023 could move to a dedicated subsection if the pre-parse validation surface grows.

Preserved strengths confirmed

  • Clause (c) preserves the conflicted lockfile rather than introducing automatic discard or re-resolution.
  • Clause (a) names path and cause without raw parser output.
  • Synthesis correction: the new requirement does not impose a different exit policy, and Appendix D 0.1.42 does not state a Section 9.2 compatibility classification. Unsupported raw-return claims to those effects were not carried forward.

Package-Manager Editor -- shocked_meter 7/10, confidence high

Summary: Diagnostic-only scope and consumer classification are coherent. Marker grammar needs precision; the heading is stale. One Appendix D nit was discarded during synthesis because its premise was false.

New recommended findings (1)

  • [pkg-rec-r1-1] req-lk-023 -- The requirement refers to unresolved version-control merge conflict markers without a minimum recognizable grammar. One implementation might diagnose a single labeled marker while another requires a complete opener/separator/closer group. The conformance oracle uses concrete Git-style input without defining the portable minimum in normative text. Existing best-effort readers and exit semantics are outside this ambiguity; this finding does not imply every command fails closed.
    Recommended fix: Add a parenthetical example list after the first mention in req-lk-023: 'unresolved version-control merge conflict markers (for example, a line beginning with seven less-than, greater-than, or pipe characters followed by a space or end-of-line)'. This aligns the normative text with the conformance oracle without excluding implementations that recognise additional VCS marker formats.

New nit findings (2)

  • [pkg-nit-r1-1] sec.5.4 -- req-lk-023 is placed inside Section 5.4 whose heading is 'Lockfile versions (1, 2) and bumping rules.' The requirement governs conflict-marker diagnosis, not version semantics. While editorially adjacent to req-lk-004 (unrecognised lockfile_version), the heading misleads implementers scanning by section title for the marker-diagnosis contract.
    Recommended fix: Defer to a future editorial pass: rename Section 5.4 to 'Lockfile versions, bumping rules, and load-time diagnostics' or extract req-lk-023 into a new subsection 5.4.1.
  • [pkg-nit-r1-2] Appendix D row 0.1.42 -- Not carried forward: this item misattributed companion Git-command wording to Appendix D. The actual row already says resolve the conflict or restore a known-good lockfile; no change to that row is warranted.

Preserved strengths confirmed

  • Clause (c) MUST-NOT ('MUST NOT remove, rewrite, or re-resolve the lockfile in response to the markers') is a proper defensive reservation that requires a future amendment to lift, preventing silent recovery drift without an explicit spec change.
  • Consumer-only conformance class assignment is correct; the requirement imposes no Producer, Registry, or Governance obligations, and the Appendix C, Section 5.7, and Section 11.3.2 enumerations are all consistent.
  • The closing scope limiter ('this requirement neither defines nor authorises automatic recovery from a conflicted lockfile') explicitly defers recovery without foreclosing it, matching the reserved-slot pattern used for workspaces, conflict_resolution:nest, and other v0.2 deferrals.
  • The diagnostic-advice clause (b) makes no false pin-preservation claim: 'resolve the conflict in that file or restore a known-good lockfile' honestly encompasses lossy single-side restoration (which discards the opposite side's recorded pins) alongside lossless manual merge, without promising that both parents' transitive resolutions survive.

TAG Architect -- shocked_meter 8/10, confidence high

Summary: req-lk-023 is well-structured, properly integrated across all conformance surfaces, and defensively scoped. Two recommended findings address cross-implementation clarity: the marker grammar is normatively referenced but never enumerated, and clause (a) embeds an untestable architectural constraint alongside testable behavioural requirements. Neither is blocking; both are cleanly deferrable to a v0.1.x followup.

New recommended findings (2)

  • [tag-rec-r1-1] req-lk-023 clause (a) -- The normative text requires a consumer to recognise 'unresolved version-control merge conflict markers' but never enumerates or references a grammar for those markers. Git two-way conflicts use lines beginning with '<<<<<<< ' and '>>>>>>> '; diff3 mode adds '||||||| '; other VCS systems use different sequences. A third-party implementer reading only the spec cannot produce an interoperable marker scanner, and two conformant implementations may disagree on what constitutes a marker. The conformance test fixture exercises only one git-style pattern, reinforcing the gap between the normative prose and testable surface.
    Recommended fix: Add an informative parenthetical or non-normative note after 'version-control merge conflict markers' in clause (a), e.g.: '(in practice, lines beginning with the seven-character sequences "<<<<<<< ", ">>>>>>> ", and optionally "||||||| ", as emitted by git and compatible tooling)'. This keeps the normative MUST behavioural while giving implementers an enumerable minimum marker set to target.
  • [tag-rec-r1-2] req-lk-023 clause (a) -- The phrase 'through the same authority that loads the lockfile' is an architectural constraint on implementation structure: it mandates that detection and loading share a code path. No conformance test can observe whether an implementation uses one code path or two -- only whether the observable output (correct detection, path in diagnostic, no raw parser spill) is correct. Normative statements should constrain observable behaviour rather than internal architecture, to avoid over-constraining implementations that use a pre-scan, a separate YAML front-end, or a streaming parser.
    Recommended fix: Replace 'through the same authority that loads the lockfile' with 'before or during the load that would otherwise attempt to parse the lockfile', or 'at lockfile-load time'. This preserves the temporal invariant (detection happens during the load phase, not later) without constraining implementation topology.

New nit findings (1)

  • [tag-nit-r1-1] req-lk-023 / Section 5.4 -- req-lk-023 is placed in Section 5.4 ('Lockfile versions (1, 2) and bumping rules') but governs pre-parse conflict-marker detection, not version semantics. The Appendix C table cites section 5.4 accordingly. A reader scanning by section topic would not look under 'versions and bumping rules' for conflict-marker handling. The placement after req-lk-004 (pre-parse version rejection) is defensible as a 'load-time rejection' cluster, but the section title does not reflect this broader scope.
    Recommended fix: Consider a one-line parenthetical in the section heading or a brief editorial note acknowledging that req-lk-023 governs a load-time pre-parse condition co-located here with req-lk-004 for that reason. Alternatively, if a future revision introduces a 'Section 5.4.1 Load-time rejection' subsection, both requirements could migrate there.

Preserved strengths confirmed

  • The new requirement, manifest entry, Appendix C row and conformance bindings agree on consumer classification.
  • The closing scope paragraph excludes unrelated corruption and automatic recovery.
  • Three conformance tests are bound to req-lk-023; this is static binding evidence, not an executed-test result.
  • Synthesis correction: the new requirement does not impose a different exit policy, and Appendix D 0.1.42 does not state a Section 9.2 compatibility classification. Unsupported raw-return claims to those effects were not carried forward.

This panel is advisory. It does not block merge. Re-apply the spec-review label after addressing feedback to re-run.


Generated by apm-spec-guardian. This comment is AI-generated and may contain errors.

…rror

Every command that reads apm.lock.yaml exits 1 with a raw PyYAML scanner
error when the file still carries conflict fences, and the --frozen tip
points at 'apm outdated' and 'apm update', which fail on the same file.
The only recovery is deleting the lockfile by hand, which re-resolves
every pin.

LockFile.read, already the single load owner, detects the markers before
parsing and raises LockfileConflictError, a LockfileFormatError. The
diagnostic names the lockfile and prints the recovery as commands the
user can run:

    git checkout apm.lock.yaml --ours # or --theirs
    apm install

Keeping one side preserves that branch's recorded pins, and the install
reconciles only what the merge added. The drift tip is suppressed when a
FrozenInstallError carries no drift reasons, so a missing or unreadable
lockfile no longer inherits advice meant for stale pins.

The conflicted lockfile is preserved byte-for-byte on every path:
install, lock, --frozen, --only, --mcp, --dry-run, update, outdated, and
lock export. A lockfile invalid for any other reason keeps its existing
handling, and a bare '=======' separator is not treated as a marker.

Tests assert the printed recipe by running it against a real merge
conflict rather than by matching its wording, so the message can be
rephrased without breaking them.

Automatic recovery is deliberately out of scope and remains deferred;
see the scope decision on microsoft#2979.

Refs microsoft#2979
auto-merge was automatically disabled September 28, 2026 14:22

Head branch was pushed to by a user without write access

@lachieh

Copy link
Copy Markdown
Contributor Author

Narrowed to the diagnostic slice and rebased onto main.

The message names the repair as commands rather than prose:

apm.lock.yaml contains unresolved git merge conflict markers.
Resolve the conflict in that file, then reinstall:
    git checkout apm.lock.yaml --ours # or --theirs, mid-merge only
    apm install

The git command resolves the file before any apm command runs, so nothing points at the unreadable lockfile. Both panel findings on the recipe are addressed: it is qualified mid-merge only (with markers committed and no merge in progress, --ours exits 0 having changed nothing), and it prints the diagnosed path rather than a bare basename, so --global and ancestor-manifest reads cannot target a different local file. The frozen handler now wraps the canonical error instead of restating it.

Tests run the printed commands against a real merge conflict rather than matching their wording, so the message can be rephrased without breaking them.

On point 4 -- the diagnostic-only diff still trips Mode B (61 lines, threshold 20), and no existing requirement covers lockfile-load diagnostics; req-lk-004 is specific to an unrecognised lockfile_version. With the waiver ruled out, req-lk-023 as pushed is that narrow amendment, for your agreement. I have folded the two spec-guardian items that all four panels converged on (the interim marker-grammar parenthetical and the BCP 14 MAY), and left the §5.4 heading rename alone since it would change the section slug.

@danielmeppiel

Copy link
Copy Markdown
Collaborator

Maintainer agreement: diagnostic-only req-lk-023

Recording Daniel Meppiel (@danielmeppiel)'s explicit human decision to approve and record req-lk-023, following presentation of the current proposed requirement and its limits.

The agreement applies to the text in docs/src/content/docs/specs/openapm-v0.1.md at a9c6e13:

  • Recognise unresolved conflict markers through the lockfile-loading authority; report the path and cause without raw parser output.
  • Direct the user to resolve the conflict or restore a known-good lockfile. An operation that reads the lockfile may be named only as a follow-up after resolution, not as the repair.
  • Preserve the lockfile bytes: do not remove, rewrite or re-resolve it in response to the markers.

The marker examples remain illustrative, not a newly fixed normative grammar. Other unreadable-lockfile conditions remain outside this requirement.

This is agreement to the proposed diagnostic-only specification amendment, not an agent review verdict. It does not approve automatic recovery, workflow execution, PR approval or merge. The bounded scope on #2979 remains unchanged, and that issue remains open. Any substantive change to the approved requirement needs renewed human agreement.


Generated by autopilot-pr-merge-worker. This comment is AI-generated and may contain errors.

@danielmeppiel

Copy link
Copy Markdown
Collaborator

APM Review Panel: ship_with_followups

Lockfile conflict-marker detection gates every consumer through the canonical load authority, diagnosing path and cause while preserving the file for manual resolution.

panel-mode=full; personas=python-architect,test-coverage-expert,devx-ux-expert,supply-chain-security-expert,doc-writer,performance-expert,cli-logging-expert,apm-ceo

cc Lachlan Heywood (@lachieh) -- a fresh advisory pass is ready for your review.

Seven panelists converge with zero blocking-severity findings; all counts are advisory. Performance and CLI-logging experts found no regressions; the conflict-marker scan is a single compiled-regex O(n) pass preceding the existing YAML parse, adding no new I/O and one additional linear scan; latency unmeasured.

The substantive findings cluster into two themes. First, repair guidance scope: devx-ux, doc-writer, and supply-chain-security all flag that the current git-checkout recipe couples the diagnostic to an active merge and does not cover the post-merge or non-git case. Supply-chain and devx-ux suggest shlex.quote for the path; however, shlex.quote is a POSIX utility that produces incorrect escaping on Windows cmd.exe and PowerShell. The already-scoped fix -- replacing the git-specific recipe with generic resolve-or-restore guidance that directs the user to retry the original invocation with its scope and flags -- subsumes the quoting concern (no shell command to quote), the mid-merge caveat gap, the doc-prose coupling, and the python-architect nit on _command_path embedding a presentation concern in a domain exception (the helper is removed when the recipe changes). Second, test strengthening: test-coverage-expert identifies three gaps (read_text universal-newline normalization masking CRLF re-encoding, missing exit-code assertion in the conformance test, absent global-scope integration test). The first two carry outcome:unknown (assertions read from source, not executed); the third carries outcome:missing. All three are bounded by source inspection, not execution evidence. The conformance-test outcome suggestion to assert exit_code != 0 across all parametrized cases must be scoped per-command: dry-run is a reading operation that intentionally exits 0 when it can present the dependency tree, so a blanket non-zero gate would be incorrect.

The normative req-lk-023 agreement is recorded at comment 5877881070 with explicit human decision; renewed human PR review and exact-final-head CI remain pending. The spec amendment is diagnostic-only as agreed; automatic recovery, normative marker grammar, and policy changes remain outside scope. The python-architect recommended follow-up to enroll lock_export in the static boundary check and add a LockFile.read mutation case is well-scoped incremental hardening. The doc-writer finding that export docs and help text still say 'apm.lock.yaml only' when resolve_lockfile_path_for_read already supports legacy apm.lock fallback is accurate and directly affected by this PR routing of lock_export through the canonical load path.

Dissent. Supply-chain-security and devx-ux both recommend shlex.quote for the printed git-checkout path. shlex.quote is POSIX-only and produces wrong escaping on Windows. The chosen fix -- generic resolve/restore guidance replacing the git recipe -- eliminates the surface entirely. I side with the generic approach: it is portable, aligns with the spec clause (b) language, and closes the mid-merge precondition gap simultaneously.

Aligned with: Conflict markers are detected before YAML parsing in every consumer; the corrupted file is never silently consumed or rewritten. req-lk-023 is recorded with explicit human agreement; frozen mode composes the diagnostic into its own error surface without inventing policy. lock_export now routes through LockFile.read via resolve_lockfile_path_for_read, closing the from_yaml bypass. Legacy apm.lock fallback works for read-only callers. One compiled-regex scan before the existing YAML parse -- no new dependencies, no new I/O, one additional linear scan (latency unmeasured), no new flags.

Panel summary

Persona B R N Takeaway
python-architect 0 1 1 Guard gap: lockfile-read boundary check covers filename resolution but misses LockFile.read load authority and lock_export consumer. One recommended follow-up, one nit.
test-coverage-expert 0 3 0 Preservation assertions use text-mode read; conformance test omits command-outcome check; no global-scope integration test.
devx-ux-expert 0 2 1 Docs couple the diagnostic description to a specific git recipe instead of the spec's generic resolve-or-restore language; troubleshooting page drops the mid-merge caveat the CLI includes.
supply-chain-security-expert 0 1 0 Conflict detection, redaction, and preservation are sound. One advice-to-shell quoting gap found.
doc-writer 0 3 0 Recovery guidance overstates applicability and pin preservation; export docs miss the legacy read-only fallback. Static review only; normative agreement remains pending.
performance-expert 0 0 0 No algorithmic or I/O regression. Conflict-marker regex is a compiled single-pass O(n) scan preceding the O(n) YAML parse on every lockfile read; no new network, cache, or materialization cost.
cli-logging-expert 0 0 0 Conflict diagnostics use correct severity levels, message structure, and CommandLogger APIs; no output UX regressions found.

B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.

Top 4 follow-ups

  1. [devx-ux-expert] Replace the git-specific recovery recipe with generic resolve-or-restore guidance; retry the original invocation preserving scope and flags; align all affected docs, help text, agent-guide, and changelog. -- Three panelists converge (devx-ux, doc-writer, supply-chain): the current recipe assumes an active git merge, does not cover post-merge or non-git scenarios, and couples six doc surfaces to a single recovery path. Generic guidance aligns with req-lk-023(b) and subsumes the path-quoting and _command_path concerns.
  2. [python-architect] Enroll lock_export in _LOCKFILE_CONSUMERS; extend the boundary check to verify LockFile.read usage; add a mutation case that kills LockFile.read delegation. -- The fix correctly routes lock_export through LockFile.read, but the static guard does not yet enforce this, leaving the bypass unprotected against silent regression.
  3. [test-coverage-expert] Strengthen preservation assertion to read_bytes in at least one parametrized case; add per-command exit-code gate in the conformance test (dry-run exits 0); add global/ancestor scope integration test. -- read_text normalizes line endings on all platforms, masking potential re-encoding. The conformance test preserves bytes but does not gate outcomes per-command. Global-scope path rendering is unit-tested but not exercised end-to-end.
  4. [doc-writer] Document legacy-lockfile input for read-only export in reference, CLI help, and agent-guide. -- resolve_lockfile_path_for_read already supports apm.lock fallback, but export docs and help still say apm.lock.yaml only. Directly affected by this PR routing of lock_export through the canonical load path.

Architecture

classDiagram
    direction LR
    class LockFile {
        <<CanonicalOwner>>
        +read(path) LockFile or None
        +from_yaml(yaml_str) LockFile
        +load_or_create(path) LockFile
        +write(path)
    }
    class LockfileFormatError {
        <<DomainException>>
    }
    class LockfileConflictError {
        <<DomainException>>
        +path Path
    }
    class UnsupportedLockfileVersionError {
        <<DomainException>>
    }
    class InstallService {
        <<Facade>>
        +enforce_frozen(request)
    }
    class FrozenInstallError {
        <<DomainException>>
        +reasons list
    }
    class resolve_lockfile_path_for_read {
        <<Pure>>
    }
    class has_conflict_markers {
        <<Pure>>
    }
    class lock_export {
        <<IOBoundary>>
    }
    class frozen_install_tip {
        <<Pure>>
    }
    LockfileFormatError <|-- LockfileConflictError
    LockfileFormatError <|-- UnsupportedLockfileVersionError
    LockFile ..> LockfileConflictError : raises
    LockFile ..> has_conflict_markers : gates parse
    LockFile ..> LockfileFormatError : raises
    InstallService ..> LockFile : reads via LockFile.read
    InstallService ..> LockfileConflictError : catches
    InstallService ..> FrozenInstallError : raises
    lock_export ..> resolve_lockfile_path_for_read : resolves path
    lock_export ..> LockFile : reads via LockFile.read
    frozen_install_tip ..> FrozenInstallError : reads reasons
    note for LockFile "Canonical load authority: read() gates\nall callers through has_conflict_markers\nbefore from_yaml"
    note for resolve_lockfile_path_for_read "Canonical filename resolution:\nmigration guard for mutating callers"
    class LockfileConflictError:::touched
    class has_conflict_markers:::touched
    class lock_export:::touched
    class LockFile:::touched
    class InstallService:::touched
    class frozen_install_tip:::touched
    classDef touched fill:#fff3b0,stroke:#d47600
Loading
flowchart TD
    A["CLI: apm lock export\nsrc/apm_cli/commands/lock.py:289"] --> B["resolve_lockfile_path_for_read root read_only=True\nsrc/apm_cli/deps/lockfile.py:1272"]
    A2["CLI: apm install --frozen\nsrc/apm_cli/install/service.py:257"] --> C["enforce_frozen request\nsrc/apm_cli/install/service.py:257"]
    B --> D["I/O LockFile.read lockfile_path\nsrc/apm_cli/deps/lockfile.py:1076"]
    C --> D
    D --> E{"path.exists?"}
    E -- No --> F["return None"]
    E -- Yes --> G["I/O path.read_text encoding utf-8\nsrc/apm_cli/deps/lockfile.py:1081"]
    G --> H{"has_conflict_markers text?\nsrc/apm_cli/deps/lockfile.py:1082"}
    H -- Yes --> I["raise LockfileConflictError path\nsrc/apm_cli/deps/lockfile.py:1083"]
    H -- No --> J["LockFile.from_yaml text\nsrc/apm_cli/deps/lockfile.py:1084"]
    J --> K{"YAML parse OK?"}
    K -- Yes --> L["return LockFile instance"]
    K -- No --> M["raise LockfileFormatError"]
    I --> N{"caller context"}
    N -- lock_export --> O["propagates to CLI error handler\nexit 1"]
    N -- enforce_frozen --> P["catch LockfileConflictError\nsrc/apm_cli/install/service.py:287"]
    P --> Q["raise FrozenInstallError\nwith conflict guidance"]
    Q --> R["frozen_install_tip error\nsrc/apm_cli/install/errors.py:96"]
    R --> S{"error.reasons empty?"}
    S -- Yes --> T["return empty string\nno misleading advice"]
    S -- No --> U["return tailored tip"]
Loading

Recommendation

The detection, preservation, and error-hierarchy design are sound. The driver is folding scoped follow-ups (generic guidance, static guard enrollment, test strengthening, export docs) into the PR before readiness. Human req-lk-023 agreement is recorded at comment 5877881070; renewed human PR review and exact-final-head CI remain pending. Local baseline at a9c6e13: 74 passed, 1 skipped, 1.99s -- does not yet cover newly promised tests.


Full per-persona findings

python-architect

  • [recommended] Static guard contracts-tooling-lockfile-read covers filename resolution but not LockFile.read load authority or lock_export consumer at scripts/architecture_linter/checks/contracts_test_taxonomy.py:94
    The architecture boundary check check_lockfile_read_resolution (contracts_test_taxonomy.py:169) verifies that four enumerated consumers in _LOCKFILE_CONSUMERS (line 94) delegate filename resolution through resolve_lockfile_path_for_read. However, it does NOT verify that consumers call LockFile.read() -- the canonical load authority that now gates all lockfile loads through has_conflict_markers before from_yaml parsing -- rather than calling LockFile.from_yaml() directly. The PR correctly fixes lock_export (lock.py:306-307) to route through both resolve_lockfile_path_for_read and LockFile.read, closing the export bypass that previously called get_lockfile_path + LockFile.from_yaml. But src/apm_cli/commands/lock.py is absent from _LOCKFILE_CONSUMERS, so the fix has no static regression protection. The mutation case (test_architecture_owner_rule_mutations.py:151) proves the guard has teeth for the read_only migration gate, but no mutation case covers a LockFile.read bypass or the lock_export consumer. Per the single-owner rule, every fix without a dual guardrail (behavioral + static) will silently regress. Follow-up should: (1) add lock.py to _LOCKFILE_CONSUMERS, (2) extend the guard to verify LockFile.read usage where from_yaml bypass is structurally reachable, and (3) add a mutation case that kills the LockFile.read delegation in one consumer. Design patterns -- Used in this PR: Base class + subclass (LockfileConflictError extends LockfileFormatError, enabling callers to catch at the granularity they need). Template Method (implicit): LockFile.read is the single entry that gates all loads through conflict detection before from_yaml, enforcing the canonical-owner rule at runtime. Pragmatic suggestion: none -- the current hierarchy is the simplest correct design at this scope.
    Suggested: Add 'src/apm_cli/commands/lock.py' to _LOCKFILE_CONSUMERS; extend check_lockfile_read_resolution to verify LockFile.read usage (not just resolve_lockfile_path_for_read); add a MutationCase that kills the LockFile.read delegation.
    Evidence: unknown / static.

  • [nit] _command_path couples CWD-relative rendering into LockfileConflictError domain exception at src/apm_cli/deps/lockfile.py:81
    _command_path (lockfile.py:81-93) computes os.path.relpath at exception-construction time and bakes the result into the error message string. This embeds a presentation concern (rendering git checkout commands relative to the invoking directory) into a domain exception. Currently tolerable: one raiser (LockFile.read:1083), two catchers (enforce_frozen in service.py:287 wraps the message; lock_export in lock.py:307 lets it propagate). If additional callers need to customize the rendered advice (e.g. a future IDE integration or JSON-output mode), the fixed string prevents re-rendering. At current scope no extraction is warranted; note for future refactoring if a third distinct rendering context appears.

test-coverage-expert

  • [recommended] Preservation assertions use read_text (universal-newline mode), cannot detect CRLF re-encoding. at tests/integration/test_install_conflicted_lockfile_e2e.py:108
    Both the integration e2e test (line 108) and the spec conformance test (line 852) assert lockfile preservation via Path.read_text(encoding='utf-8'), which applies Python universal-newline translation (\r\n -> \n on all OSes, not just Windows). The PR body and req-lk-023(c) promise 'byte-for-byte' preservation, but this assertion would pass even if a regression rewrote the file with different line endings. A read_bytes() comparison would be strictly stronger. Probed: grep'd all three new test files for read_bytes and newline= -- zero matches for lockfile preservation; the conformance file uses read_bytes only for unrelated trust-archive paths (lines 241, 288, 652, 672).
    Suggested: Replace the read_text preservation assertion with read_bytes() and compare against _CONFLICTED_LOCKFILE.encode('utf-8') in at least one parametrized case, e.g.: assert (conflicted_project / 'apm.lock.yaml').read_bytes() == _CONFLICTED_LOCKFILE.encode('utf-8').
    Evidence: unknown / integration-with-fixtures.

  • [recommended] Conformance test for req-lk-023(c) does not assert command outcome (exit code or exception type). at tests/spec_conformance/test_lockfile_reqs.py:849
    test_lockfile_is_left_unmodified_across_reading_operations invokes four command sets with catch_exceptions=True (line 849) but never asserts result.exit_code or isinstance(result.exception, LockfileConflictError). The preservation assertion alone would pass under a regression that changes the diagnostic shape (e.g. raw YAMLError instead of named LockfileConflictError) or even if the command silently succeeds and happens not to write the file. The companion e2e test does assert exit_code == 1, but the conformance test -- the one mapped to the ratified spec clause -- does not carry a parallel outcome gate. Probed: grep'd test_lockfile_reqs.py for exit_code and exception near the req-lk-023(c) test -- zero matches inside that test function.
    Suggested: After the invoke, add: assert result.exit_code != 0, f'{args} MUST fail when the lockfile contains conflict markers'.
    Evidence: unknown / integration-with-fixtures.

  • [recommended] No integration test exercises a conflicted lockfile in global or ancestor scope. at tests/integration/test_install_conflicted_lockfile_e2e.py
    All new tests create a project-scope apm.lock.yaml in tmp_path. The production code _command_path() renders an absolute path when the lockfile is outside the working directory (tested at unit tier in test_recovery_command_targets_a_lockfile_outside_the_working_directory), but no Click CliRunner test exercises apm install -g or an ancestor lockfile via the full command flow. Probe: grep'd tests/ for global.*conflict, conflict.*global, --global.*conflict, -g.*conflict -- only unrelated MCP conflict detection and Docker installer files matched (tests/unit/test_conflict_detection.py, tests/unit/test_docker_args_and_installer.py).
    Suggested: Add a parametrized case to the e2e test that places the conflicted lockfile at a user-scope path and invokes install -g, asserting exit_code == 1 and the absolute path in the diagnostic.
    Evidence: missing / integration-with-fixtures.

devx-ux-expert

  • [recommended] Reference docs describe the diagnostic as naming a specific git command, coupling prose to the current recipe. at docs/src/content/docs/reference/lockfile-spec.md:412
    req-lk-023 clause (b) deliberately says 'resolve the conflict in that file or restore a known-good lockfile' -- generic, not git-specific. lockfile-spec.md line 412 and install.md line 165 both say 'name the git command that keeps one side of the merge', tying the user-facing reference doc to the current git-checkout recipe. If the recipe is revised to generic resolve/restore guidance, these doc paragraphs require parallel edits. Reference docs are what a confused user reads first; they should reflect the spec's broader framing rather than pin a single recovery path.
    Suggested: Replace 'name the git command that keeps one side of the merge so you can reinstall from it' with language closer to the spec: 'direct you to resolve the conflict or restore a known-good lockfile before reinstalling'. Apply the same change in install.md's frozen-mode bullet.
    Evidence: unknown / static.

  • [recommended] Troubleshooting page drops the mid-merge caveat from the recovery recipe. at docs/src/content/docs/troubleshooting/install-failures.md:185
    The CLI diagnostic at lockfile.py:104 says '# or --theirs, mid-merge only' but install-failures.md line 185 shows only '# or --theirs'. git checkout --ours/--theirs requires an active merge; outside an active merge git returns 'error: --ours/--theirs is incompatible with switching branches'. A user who committed the conflicted file and encounters this page later gets a confusing git error with no fallback guidance. The CLI is more honest about the precondition; the doc should match or offer an alternative for the post-merge case.
    Suggested: Add '# mid-merge only' to the comment, matching the CLI output, or add a sentence noting that outside an active merge the user can restore from version control instead (e.g., git checkout main -- apm.lock.yaml).
    Evidence: unknown / static.

  • [nit] Recovery recipe does not quote the lockfile path in the printed git command. at src/apm_cli/deps/lockfile.py:104
    The _command_path helper returns an unquoted path string. For the common project-relative apm.lock.yaml this is safe, but a user-scope lockfile under a home directory with spaces (e.g., C:\Users\John Doe...) produces a git command that splits on whitespace. The printed recipe is the one concrete action the error offers; a broken command on the first try undermines the recovery UX.
    Suggested: Quote the target path in the printed recipe, e.g., git checkout "{target}" --ours, or use shlex.quote(target) for the display string.
    Evidence: unknown / static.

supply-chain-security-expert

  • [recommended] Shell recipe interpolates the lockfile path unquoted into a git checkout command. at src/apm_cli/deps/lockfile.py:104
    LockfileConflictError.init embeds _command_path(path) directly into the git checkout recipe without shell quoting. If the resolved path contains spaces or shell metacharacters such as dollar signs, a user who copy-pastes the advice gets a broken or misinterpreted command. The test helper diagnostic_recipe.py already imports shlex and splits safely, but the production side that composes the recipe does not quote. shlex.quote(target) would close this gap for all path spellings.
    Suggested: Add 'import shlex' to module imports; change line 104 to: f" git checkout {shlex.quote(target)} --ours # or --theirs, mid-merge only\n"
    Evidence: manual / static.

doc-writer

  • [recommended] Replace the unconditional recovery recipe with context-safe manual guidance. at docs/src/content/docs/troubleshooting/install-failures.md:185
    The troubleshooting recipe and packages/apm-guide/.apm/skills/apm-usage/troubleshooting.md:14 omit the 'mid-merge only' qualification now present in src/apm_cli/deps/lockfile.py:104. Conflict markers can survive in a committed file with no unmerged index stages, so selecting --ours or --theirs is not a general repair. The basename also assumes the lockfile is in the current directory. The subsequent plain 'apm install' loses the original command and scope: src/apm_cli/commands/install.py:1357 selects project scope unless --global is supplied, so a user-scope failure can lead to installing an unrelated project. The frozen PR body further contradicts this head by saying the diagnostic 'deliberately names no command'. These are static observations; no recipe or contribution tests were executed.
    Suggested: Make manual resolution of the named file or restoration of a known-good lockfile the general instruction, followed by retrying the original invocation with its scope and flags. If retaining a Git example, explicitly restrict it to an active merge in the owning repository. Align the canonical diagnostic, troubleshooting guide, agent guide, reference summaries, changelog, and PR body without adding a recovery command or policy.
    Evidence: unknown / static.

  • [recommended] Do not promise that selecting one side preserves all pins through reinstall. at docs/src/content/docs/troubleshooting/install-failures.md:189
    The claim that reinstall 'resolves only what the merge added' is stronger than the existing replay contract: docs/src/content/docs/reference/cli/install.md:162 limits locked-commit reuse to unchanged Git dependencies. A merge can change an existing dependency's ref, not just add dependencies. Selecting one whole lockfile side also drops records unique to the other side; the agent-guide troubleshooting row's assurance that pins and deployment records 'are not lost' obscures that distinction. The new recipe test invokes run_recipe with only='git' and then checks that LockFile.read succeeds; it does not run the follow-up install or establish post-repair pin preservation. No tests were executed in this review.
    Suggested: State that APM leaves the conflicted file unchanged until the user repairs it. Explain that selecting a side retains only that side's records and that changed or missing dependencies may resolve again; require reviewing the repaired lockfile against the merged manifest before committing. Replace the unconditional agent-guide assurance with the same bounded statement and link to the existing replay contract.
    Evidence: unknown / static.

  • [recommended] Document the newly supported legacy lockfile input for read-only export. at docs/src/content/docs/reference/cli/lock.md:79
    The export reference still says it reads 'apm.lock.yaml only'. At this head, src/apm_cli/commands/lock.py:306 calls resolve_lockfile_path_for_read(project_root, read_only=True), whose implementation in src/apm_cli/deps/lockfile.py:1272-1282 selects apm.lock when apm.lock.yaml is absent without renaming it. The same stale input restriction remains in both export rows of packages/apm-guide/.apm/skills/apm-usage/commands.md:440 and :442 and in the export help at src/apm_cli/commands/lock.py:252. Readers with a legacy lockfile are not told that they can export directly without first generating or migrating a lockfile. This is verified from source only; no export invocation or tests were executed.
    Suggested: State once in the export reference that apm.lock.yaml takes precedence and legacy apm.lock is read in place when the current filename is absent. Align the CLI help and agent-guide export descriptions with that behavior, preserving the no-resolution, no-rehash, and no-network guarantees.
    Evidence: unknown / static.

performance-expert

No findings.

cli-logging-expert

No findings.

This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.


Generated by autopilot-pr-review-worker. This comment is AI-generated and may contain errors.

@danielmeppiel

Copy link
Copy Markdown
Collaborator

APM Spec Guardian: fold_and_ship

Scope: editorial-patch; diff = +40/-3 lines across 1 file(s). Shocked-meter avg: 8.0/10.

req-lk-023 is a diagnostic-only amendment approved by the maintainer (comment 5877881070); no broad normative change was accepted. All four panels rate it 8/10 with zero blockers and unanimous ship_with_followups, matching the v0.1 round-2 calibration anchor at mean 8.0. The two fold-now items are mechanical: reformat the Note paragraph as a blockquote after the complete requirement (T1, four-panel convergence), and insert a one-sentence editorial bridge before req-lk-023 to aid section-local discoverability (T2, two-panel convergence). Both preserve existing normative text, slug stability, and the illustrative grammar exactly. The marker-grammar interop gap (T3) is real but explicitly outside this PR's approved diagnostic-only scope; both related findings are declined as requiring separately scoped human agreement. The Section 5.4 heading/slug churn originally deferred is no longer needed because the F2 editorial bridge satisfies the discoverability concern that motivated it. This synthesis is advisory on the specification stance only; renewed human review of the specification text and functional/CI validation remain separate gates. Note: the Swagger panel's sw-rec-r1-1 references 'Appendix D section column' for the index table; the source artifact places the index in Appendix C and revision history in Appendix D -- this factual correction does not affect any fold or finding.

Convergence

Panel Stance Shocked New B New R New N
spec-swagger-editor ship_with_followups 8/10 0 1 1
spec-oci-editor ship_with_followups 8/10 0 0 1
spec-pkgmgr-editor ship_with_followups 8/10 0 1 1
spec-tag-architect ship_with_followups 8/10 0 2 1

B = new blocking findings, R = new recommended, N = new nits.
Counts are signal strength, not gates. The maintainer ships.

Convergent themes (flagged by 2+ panels)

  • T1 -- Note paragraph should use the spec's established blockquote convention and sit after the complete requirement (supporting: sw-nit-r1-1, oci-nit-r1-1, pkg-nit-r1-1, tag-nit-r1-1)
  • T2 -- Section 5.4 heading does not cover conflict-marker detection; thematic bridge needed for section-local readers (supporting: sw-rec-r1-1, tag-rec-r1-1)
  • T3 -- Unpinned marker grammar leaves a conformance-testing interop gap that a future amendment should close (supporting: pkg-rec-r1-1, tag-rec-r1-2)

Fold now (2 item(s))

  1. [F1 / T1] sec.5.4 / req-lk-023 -- Move the free-standing 'Note -- that parenthetical is an interim illustrative aid; the normative minimum marker grammar is not yet pinned by this specification.' paragraph from its current position between the MUST lead-in and clause (a) to immediately after the closing scope paragraph ('This requirement governs diagnosis only ... automatic recovery from a conflicted lockfile.'). Reformat it as a blockquote matching the spec's established convention: '> Note. The parenthetical above is an interim illustrative aid; the normative minimum marker grammar is not yet pinned by this specification.' Preserve the exact semantic content; change only placement and markup.
    Success criterion: Grep for '^> \*\*Note\.\*\*.*interim illustrative aid' in the req-lk-023 block returns exactly one hit, located after the line containing 'neither defines nor authorises automatic recovery' and before Section 5.5. No free-standing 'Note --' paragraph remains between the MUST lead-in and clause (a).

  2. [F2 / T2] sec.5.4 / between req-lk-004 and req-lk-023 -- Insert a one-sentence editorial bridge paragraph after the closing text of req-lk-004 ('regenerating the lockfile from the manifest.') and before the '' anchor: 'This section also covers diagnosis of unresolved merge-conflict markers at load time.' This preserves the existing Section 5.4 slug and heading unchanged.
    Success criterion: Grep for 'diagnosis of unresolved merge-conflict markers at load time' in the spec returns exactly one hit, located between 'regenerating the lockfile from the manifest.' and '<a id="req-lk-023">'.

Findings not carried forward

  • pkg-rec-r1-1 -- Pinning a normative minimum marker grammar requires separately scoped human agreement and a dedicated amendment. The claim that real git conflict markers 'always produce invalid YAML' is not guaranteed -- markers inside a YAML literal block scalar or a multi-line quoted value can be syntactically valid YAML, so the practical divergence is wider than error-message quality alone. The current illustrative parenthetical is intentionally non-normative. Narrowing the interop gap is desirable but exceeds this PR's approved diagnostic-only scope. Deferred as a future amendment subject to its own issue and approval, not bound to a version timetable.

  • tag-rec-r1-2 -- Appending a forward-compatibility sentence naming a future amendment slot or issue obligation would create a version-timetable commitment that has not been scoped or agreed by the maintainer. The existing Note paragraph already states the grammar 'is not yet pinned by this specification', which is a sufficient convergence signal. Adding an explicit amendment reference or tracking issue requires renewed human agreement outside this advisory.

Linter notes (1 check(s) failed)

  • [10] Unreleased changelog does not mention spec file path; this is an advisory SHOULD

Synthesizer recommends editorial folds; the linter found the changelog reference worth folding in the same pass.

Linter handoff: After F1 lands, grep '^> **Note.**.*interim illustrative aid' in the req-lk-023 block should return exactly one hit after the closing scope paragraph. No free-standing 'Note --' paragraph should remain between the MUST lead-in and clause (a). After F2 lands, grep 'diagnosis of unresolved merge-conflict markers at load time' returns exactly one hit between the req-lk-004 closing paragraph and the req-lk-023 anchor. Confirm Appendix C row count and Section 1.3 count remain 124 (119 MUST, 5 SHOULD) -- F1 and F2 are editorial and do not change statement count. Confirm six #req-lk-023 anchor references still resolve across Section 5.7, Section 11.3.2, Appendix C, and the Appendix D revision-history row.
Checks 1-7 and 9 passed; check 8 has no Mermaid diagrams. Check 11 is mixed Python scope: baseline diagnostics ran (74 passed, 1 skipped), but the full lint chain is not yet executed.


Full per-panel findings

spec-swagger-editor -- shocked_meter 8/10, confidence high

Summary: Clean addition. req-lk-023 is correctly indexed, counted, and classified across all six locations; RFC 2119 discipline is sound. One recommended follow-up: the section heading under which it sits does not cover conflict-marker detection.

New recommended findings (1)

  • [sw-rec-r1-1] sec.5.4 / Appendix D req-lk-023 row -- req-lk-023 is placed in Section 5.4 whose heading is 'Lockfile versions (1, 2) and bumping rules'. The requirement governs merge-conflict-marker detection, which is a pre-parse concern orthogonal to lockfile schema versioning (req-lk-002 / req-lk-004). The Appendix D section column faithfully records '5.4', but a consumer implementer looking for conflict-marker requirements would not search a version-bumping section. The brief notes the title was deliberately left unchanged to avoid slug breakage, so this is a follow-up concern rather than a fold-now item.
    Recommended fix: In a follow-up revision (not this PR), consider either renaming Section 5.4 to a broader title such as 'Lockfile load-time conditions' or relocating req-lk-023 to its own subsection (e.g. 5.4.1) with a heading that names the concern.

New nit findings (1)

  • [sw-nit-r1-1] req-lk-023 Note paragraph (spec line 1179) -- The informative 'Note -- that parenthetical is an interim illustrative aid ...' sits between the umbrella MUST clause and the sub-clause enumeration (a)/(b)/(c). This is the only occurrence of the 'Note --' pattern in the spec. Interposing an informative paragraph between 'MUST satisfy all of the following:' and the first lettered clause interrupts the normative flow a reader expects to follow immediately.
    Recommended fix: Move the Note to immediately after sub-clause (c) and the trailing scope paragraph (before Section 5.5), or convert it to a Markdown blockquote (> Note: ...) so it is visually distinct from normative text.

Preserved strengths confirmed

  • Count consistency: Sec 1.3, Appendix C trailer, and Appendix D revision-history all agree on 124 normative statements (119 MUST, 5 SHOULD); the Appendix D table row count independently confirms 119+5=124.
  • Conformance class enumeration: req-lk-023 is correctly classified as consumer MUST in Section 5.7, Section 11.3.2 (Appendix C Consumer list), Appendix D table, CONFORMANCE.json, CONFORMANCE.md, and requirements.yml.
  • Anchor stability: req-lk-023 takes the next free numeric slot (after req-lk-022) with no renumbering of existing ids.
  • RFC 2119 keyword discipline: every normative claim in the new text carries an explicit MUST, MUST NOT, or MAY; no lowercase informal usage of normative keywords in the added text.
  • Cross-reference accuracy: all six #req-lk-023 anchor references across Section 5.7, Section 11.3.2, Appendix C listing, and Appendix D table resolve to the single tag in Section 5.4.

spec-oci-editor -- shocked_meter 8/10, confidence high

Summary: req-lk-023 is a clean diagnostic-only amendment. Hash envelopes, content addressing, extraction caps, and the supply-chain threat model are untouched. Clause (c) correctly preserves lockfile bytes.

New nit findings (1)

  • [oci-nit-r1-1] req-lk-023 -- The freestanding 'Note -- that parenthetical is an interim illustrative aid' paragraph between req-lk-023's opening MUST and clause (a) does not use the blockquote '> Editorial note.' convention established throughout the spec (Sections 5.6.4, 8.5.1, 8.5.5, and twelve other instances). Consistent blockquote formatting would help implementers distinguish non-normative guidance from normative text when scanning clause boundaries.
    Recommended fix: Reformat the Note paragraph as a blockquote: '> Note. That parenthetical is an interim illustrative aid; the normative minimum marker grammar is not yet pinned by this specification.'

Preserved strengths confirmed

  • Hash envelope anchoring (req-lk-016) intact: sha256:[0-9a-f]{64} pattern untouched by this amendment
  • Canonical content addressing (req-lk-012, req-lk-015) with tree_sha256 edge-case enumeration intact
  • Fail-closed extraction (req-sc-004, req-sc-005) with media-type pinning and decompression caps intact
  • Supply-chain threat model (Section 10) with full req-XXX mapping intact; req-lk-023 correctly omitted from Section 10.4 because conflict markers are a VCS workflow hazard not an adversarial vector
  • Clause (c) of req-lk-023 correctly prevents silent lockfile regeneration which would undermine recorded-hash integrity

spec-pkgmgr-editor -- shocked_meter 8/10, confidence high

Summary: req-lk-023 is a well-scoped diagnostic-only amendment that preserves lockfile determinism and explicitly reserves the recovery slot. The sole interoperability gap is the deferred marker grammar; a follow-up amendment pinning the minimum set would close it. No blocking findings.

New recommended findings (1)

  • [pkg-rec-r1-1] req-lk-023 clause (a) -- The marker detection grammar is explicitly deferred ('the normative minimum marker grammar is not yet pinned by this specification'), so two conforming consumers can disagree on whether a lockfile contains conflict markers. While real git conflict markers always produce invalid YAML and the practical divergence is limited to error-message quality, the gap means a conformance test suite cannot black-box verify clause (a) against edge-case inputs (e.g., pipe markers, markers inside YAML literal block scalars, or unusual whitespace after the seven-character prefix). A follow-up amendment pinning at least the three canonical git marker prefixes with their line-start anchor would close this interop gap and make the conformance oracle deterministic.
    Recommended fix: In a follow-up amendment (not this PR), add a normative minimum marker set to req-lk-023 clause (a), for example: 'At minimum, lines beginning with seven consecutive U+003C, seven consecutive U+003E, or seven consecutive U+007C, each followed by U+0020 or end-of-line, MUST be recognised as conflict markers.' Leave the current illustrative parenthetical as-is for now and convert it to normative text in that amendment.

New nit findings (1)

  • [pkg-nit-r1-1] req-lk-023, Note paragraph -- The free-standing 'Note -- that parenthetical is an interim illustrative aid' paragraph sits between the MUST lead-in and clause (a) without the spec's established editorial-note markup ('> Editorial note.' or '> Note.'). A reader skimming for normative clauses could misread it as modifying the MUST above.
    Recommended fix: Reformat as a blockquote editorial note consistent with the rest of the document: '> Note. The parenthetical above is an interim illustrative aid; the normative minimum marker grammar is not yet pinned by this specification.'

Preserved strengths confirmed

  • Clause (c) preservation contract (MUST NOT remove, rewrite, or re-resolve) prevents silent lockfile mutation, matching the lockfile-determinism discipline of the existing req-lk-005/req-lk-006 surface.
  • The requirement is correctly scoped to the consumer conformance class and placed in Section 5.4, consistent with the existing lockfile-load authority chain (req-lk-004).
  • The closing paragraph explicitly disclaims recovery authority, functioning as a defensive reserved-slot statement that prevents an implementer from treating this requirement as license for automatic recovery.
  • Clause (b) MUST NOT for read-same-file operations is a sound lockfile-UX contract: it prevents the circular-advice anti-pattern where a tool directs the user to an operation that fails on the same unreadable file.

spec-tag-architect -- shocked_meter 8/10, confidence high

Summary: req-lk-023 is well-scoped and correctly integrated across all projection surfaces. Two recommended findings: the section-5.4 heading mismatch dilutes the heading's semantic contract for section-local readers, and the unpinned marker grammar leaves an interoperability gap that should reserve an explicit future-amendment slot. Neither is blocking; the spec remains self-contained and the amendment process is followed cleanly.

New recommended findings (2)

  • [tag-rec-r1-1] sec.5.4 / req-lk-023 -- req-lk-023 (conflict-marker detection, a diagnostic requirement) is placed under Section 5.4 whose heading is 'Lockfile versions (1, 2) and bumping rules'. The other two requirements in that section (req-lk-002, req-lk-004) are exclusively about version selection and version refusal. A third-party implementer reading the section heading to locate version-handling obligations would not expect to find a pre-parse diagnostic requirement there. Appendix C and Section 5.7 both index the requirement correctly, so discoverability via the table is intact, but the heading's semantic contract is diluted for any section-local reader. The slug-stability rationale for not renaming the section is understood; the concern is about placement of the requirement, not the title.
    Recommended fix: In a follow-up amendment, insert a subsection heading (e.g. '5.4.1 Pre-parse lockfile conditions') immediately before req-lk-023 to signal the thematic shift within the existing section slug. Alternatively, add a one-sentence editorial bridge after req-lk-004: 'The following requirement addresses a second condition -- unresolved merge-conflict markers -- that prevents the lockfile from being loaded.' Either approach preserves the 5.4 slug while restoring heading-level coherence.
  • [tag-rec-r1-2] sec.5.4 / req-lk-023 Note paragraph -- req-lk-023 mandates detection of 'unresolved version-control merge conflict markers' (a MUST), yet the only characterisation of what bytes constitute such a marker is the illustrative parenthetical, which the immediately following Note paragraph disclaims as non-normative. Two conforming implementations can therefore disagree on what byte sequence constitutes a merge-conflict marker and both claim conformance. Git alone produces markers in two-way, diff3, and zdiff3 modes with varying anatomy; other version-control systems use different conventions. The gap is acknowledged and intentional, but the spec gives the implementer no signal about when or how the grammar will be pinned, nor any convergence anchor in the interim.
    Recommended fix: Append a sentence to the Note paragraph reserving the grammar pin as a named future amendment slot, e.g.: 'A normative minimum marker grammar is deferred to a future v0.1.x amendment and will be tracked under the originating issue.' This gives implementers a concrete forward-compatibility signal without pinning any interim detection grammar as a conformance floor.

New nit findings (1)

  • [tag-nit-r1-1] sec.5.4 / req-lk-023 Note -- The 'Note -- that parenthetical...' paragraph uses informal inline prose for a normative disclaimer. Other load-bearing notes in the spec use block-quote or explicit callout formatting. Using the same convention here would prevent an implementer from accidentally treating the illustrative parenthetical as the normative detection grammar.
    Recommended fix: Reformat as a block-quote note: '> Note: The parenthetical above is an interim illustrative aid; the normative minimum marker grammar is not yet pinned by this specification.' to match the spec's existing callout conventions.

Preserved strengths confirmed

  • Amendment process (Section 9.3, Appendix D): the 0.1.42 entry is dated, cross-referenced, carries a statement-count diff, and follows the established revision-history pattern through 0.1.41.
  • Machine-readable projection consistency: requirements.yml, CONFORMANCE.json, CONFORMANCE.md, Appendix C table, and Section 5.7 / 11.3.2 enumerations all agree on req-lk-023 id, keyword (MUST), section (5.4), and conformance class (consumer).
  • Diagnosis-only scoping clause at the end of req-lk-023 explicitly disclaims automatic recovery and narrows the requirement to detection and reporting only, protecting forward compatibility for a future recovery amendment.
  • Three-clause (a/b/c) structure is self-contained, independently testable, and does not require reading any companion document to implement.

This panel is advisory. It does not block merge. Re-apply the spec-review label after addressing feedback to re-run.


Generated by apm-spec-guardian. This comment is AI-generated and may contain errors.

Address the current general panel's manual-repair, canonical export routing, byte-preservation and documentation follow-ups. Replace the Git recipe with resolve-or-restore guidance followed by the original invocation; protect LF/CRLF, scope and legacy reads with behavioral and architecture regressions. Apply the specification panel's editorial folds without changing the human-agreed req-lk-023 obligations. References microsoft#2979 without closing its deferred automatic-recovery work.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/cli CLI command surface, flags, help text (cross-cutting). area/lockfile Lockfile schema, per-file provenance, integrity hashes, drift detection. status/accepted Human scope approval; verify the issue's approval record and review contact before work. theme/security Secure by default. Content scanning, lockfile integrity, MCP trust boundaries. triage/recommended Automated advice completed; not human scope approval. type/bug Something does not work as documented.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants